Add historical data
This commit is contained in:
parent
2b90e9b11e
commit
b16f836f0e
195
README.md
195
README.md
@ -0,0 +1,195 @@
|
|||||||
|
# Cryptocurrency Market Data MCP Server
|
||||||
|
|
||||||
|
A Model Context Protocol (MCP) server that provides real-time and historical cryptocurrency market data through integration with major exchanges. This server enables LLMs like Claude to fetch current prices, analyze market trends, and access detailed trading information.
|
||||||
|
|
||||||
|
[](https://modelcontextprotocol.io)
|
||||||
|
[](https://www.python.org)
|
||||||
|
[](https://github.com/ccxt/ccxt)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Real-time Market Data**
|
||||||
|
- Current cryptocurrency prices
|
||||||
|
- Market summaries with bid/ask spreads
|
||||||
|
- Top trading pairs by volume
|
||||||
|
- Multiple exchange support
|
||||||
|
|
||||||
|
- **Historical Analysis**
|
||||||
|
- OHLCV (candlestick) data
|
||||||
|
- Price change statistics
|
||||||
|
- Volume history tracking
|
||||||
|
- Customizable timeframes
|
||||||
|
|
||||||
|
- **Exchange Support**
|
||||||
|
- Binance
|
||||||
|
- Coinbase
|
||||||
|
- Kraken
|
||||||
|
- KuCoin
|
||||||
|
- HyperLiquid
|
||||||
|
- Huobi
|
||||||
|
- Bitfinex
|
||||||
|
- Bybit
|
||||||
|
- OKX
|
||||||
|
- MEXC
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using uv (recommended)
|
||||||
|
uv pip install mcp ccxt
|
||||||
|
|
||||||
|
# Using pip
|
||||||
|
pip install mcp ccxt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Running the Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python crypto_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connecting with Claude Desktop
|
||||||
|
|
||||||
|
1. Open your Claude Desktop configuration at:
|
||||||
|
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||||
|
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||||
|
|
||||||
|
2. Add the server configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"crypto": {
|
||||||
|
"command": "python",
|
||||||
|
"args": ["/path/to/crypto_server.py"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Restart Claude Desktop
|
||||||
|
|
||||||
|
### Available Tools
|
||||||
|
|
||||||
|
1. **get-price**
|
||||||
|
- Get current price for any trading pair
|
||||||
|
- Example: "What's the current price of BTC/USDT on Binance?"
|
||||||
|
|
||||||
|
2. **get-market-summary**
|
||||||
|
- Fetch detailed market information
|
||||||
|
- Example: "Show me a market summary for ETH/USDT"
|
||||||
|
|
||||||
|
3. **get-top-volumes**
|
||||||
|
- List top trading pairs by volume
|
||||||
|
- Example: "What are the top 5 trading pairs on Kraken?"
|
||||||
|
|
||||||
|
4. **list-exchanges**
|
||||||
|
- Show all supported exchanges
|
||||||
|
- Example: "Which exchanges are supported?"
|
||||||
|
|
||||||
|
5. **get-historical-ohlcv**
|
||||||
|
- Get historical candlestick data
|
||||||
|
- Example: "Show me the last 7 days of BTC/USDT price data in 1-hour intervals"
|
||||||
|
|
||||||
|
6. **get-price-change**
|
||||||
|
- Calculate price changes over different timeframes
|
||||||
|
- Example: "What's the 24-hour price change for SOL/USDT?"
|
||||||
|
|
||||||
|
7. **get-volume-history**
|
||||||
|
- Track trading volume over time
|
||||||
|
- Example: "Show me the trading volume history for ETH/USDT over the last week"
|
||||||
|
|
||||||
|
### Example Queries
|
||||||
|
|
||||||
|
Here are some example questions you can ask Claude once the server is connected:
|
||||||
|
|
||||||
|
```
|
||||||
|
- What's the current Bitcoin price on Binance?
|
||||||
|
- Show me the top 5 trading pairs by volume on Coinbase
|
||||||
|
- How has ETH/USDT performed over the last 24 hours?
|
||||||
|
- Give me a detailed market summary for SOL/USDT on Kraken
|
||||||
|
- What's the trading volume history for BNB/USDT over the last week?
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- `mcp`: Model Context Protocol SDK
|
||||||
|
- `ccxt`: Cryptocurrency Exchange Trading Library
|
||||||
|
- Python 3.9 or higher
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
The server uses:
|
||||||
|
- CCXT's async support for efficient exchange communication
|
||||||
|
- MCP's tool system for LLM integration
|
||||||
|
- Standardized data formatting for consistent outputs
|
||||||
|
- Connection pooling for optimal performance
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
The server implements robust error handling for:
|
||||||
|
- Invalid trading pairs
|
||||||
|
- Exchange connectivity issues
|
||||||
|
- Rate limiting
|
||||||
|
- Malformed requests
|
||||||
|
- Network timeouts
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# To be implemented
|
||||||
|
pytest tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Submit a pull request
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone [repository-url]
|
||||||
|
cd crypto-mcp-server
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
uv pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Exchange Connection Errors**
|
||||||
|
- Check your internet connection
|
||||||
|
- Verify the exchange is operational
|
||||||
|
- Ensure the trading pair exists on the selected exchange
|
||||||
|
|
||||||
|
2. **Rate Limiting**
|
||||||
|
- Implement delays between requests
|
||||||
|
- Use different exchanges for high-frequency queries
|
||||||
|
- Check exchange-specific rate limits
|
||||||
|
|
||||||
|
3. **Data Formatting Issues**
|
||||||
|
- Verify trading pair format (e.g., BTC/USDT, not BTCUSDT)
|
||||||
|
- Check timeframe specifications
|
||||||
|
- Ensure numerical parameters are within valid ranges
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - See LICENSE file for details
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
- [CCXT](https://github.com/ccxt/ccxt) for exchange integrations
|
||||||
|
- [Model Context Protocol](https://modelcontextprotocol.io) for the MCP specification
|
||||||
|
- The cryptocurrency exchanges for providing market data APIs
|
||||||
189
src/server.py
189
src/server.py
@ -5,6 +5,7 @@ import mcp.types as types
|
|||||||
from mcp.server import Server, NotificationOptions
|
from mcp.server import Server, NotificationOptions
|
||||||
from mcp.server.models import InitializationOptions
|
from mcp.server.models import InitializationOptions
|
||||||
import mcp.server.stdio
|
import mcp.server.stdio
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
# Initialize server
|
# Initialize server
|
||||||
server = Server("crypto-server")
|
server = Server("crypto-server")
|
||||||
@ -15,7 +16,7 @@ SUPPORTED_EXCHANGES = {
|
|||||||
'coinbase': ccxt.coinbase,
|
'coinbase': ccxt.coinbase,
|
||||||
'kraken': ccxt.kraken,
|
'kraken': ccxt.kraken,
|
||||||
'kucoin': ccxt.kucoin,
|
'kucoin': ccxt.kucoin,
|
||||||
'ftx': ccxt.hyperliquid,
|
'hyperliquid': ccxt.hyperliquid,
|
||||||
'huobi': ccxt.huobi,
|
'huobi': ccxt.huobi,
|
||||||
'bitfinex': ccxt.bitfinex,
|
'bitfinex': ccxt.bitfinex,
|
||||||
'bybit': ccxt.bybit,
|
'bybit': ccxt.bybit,
|
||||||
@ -26,6 +27,7 @@ SUPPORTED_EXCHANGES = {
|
|||||||
# Exchange instances cache
|
# Exchange instances cache
|
||||||
exchange_instances = {}
|
exchange_instances = {}
|
||||||
|
|
||||||
|
|
||||||
async def get_exchange(exchange_id: str) -> ccxt.Exchange:
|
async def get_exchange(exchange_id: str) -> ccxt.Exchange:
|
||||||
"""Get or create an exchange instance."""
|
"""Get or create an exchange instance."""
|
||||||
exchange_id = exchange_id.lower()
|
exchange_id = exchange_id.lower()
|
||||||
@ -38,6 +40,7 @@ async def get_exchange(exchange_id: str) -> ccxt.Exchange:
|
|||||||
|
|
||||||
return exchange_instances[exchange_id]
|
return exchange_instances[exchange_id]
|
||||||
|
|
||||||
|
|
||||||
async def format_ticker(ticker: Dict[str, Any], exchange_id: str) -> str:
|
async def format_ticker(ticker: Dict[str, Any], exchange_id: str) -> str:
|
||||||
"""Format ticker data into a readable string."""
|
"""Format ticker data into a readable string."""
|
||||||
return (
|
return (
|
||||||
@ -52,6 +55,7 @@ async def format_ticker(ticker: Dict[str, Any], exchange_id: str) -> str:
|
|||||||
"---"
|
"---"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_schema() -> Dict[str, Any]:
|
def get_exchange_schema() -> Dict[str, Any]:
|
||||||
"""Get the JSON schema for exchange selection."""
|
"""Get the JSON schema for exchange selection."""
|
||||||
return {
|
return {
|
||||||
@ -61,10 +65,43 @@ def get_exchange_schema() -> Dict[str, Any]:
|
|||||||
"default": "binance"
|
"default": "binance"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def format_ohlcv_data(ohlcv_data: List[List], timeframe: str) -> str:
|
||||||
|
"""Format OHLCV data into a readable string with price changes."""
|
||||||
|
formatted_data = []
|
||||||
|
|
||||||
|
for i, candle in enumerate(ohlcv_data):
|
||||||
|
timestamp, open_price, high, low, close, volume = candle
|
||||||
|
|
||||||
|
# Calculate price change from previous close if available
|
||||||
|
price_change = ""
|
||||||
|
if i > 0:
|
||||||
|
prev_close = ohlcv_data[i-1][4]
|
||||||
|
change_pct = ((close - prev_close) / prev_close) * 100
|
||||||
|
price_change = f"Change: {change_pct:+.2f}%"
|
||||||
|
|
||||||
|
# Format the candle data
|
||||||
|
dt = datetime.fromtimestamp(timestamp/1000).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
candle_str = (
|
||||||
|
f"Time: {dt}\n"
|
||||||
|
f"Open: {open_price:.8f}\n"
|
||||||
|
f"High: {high:.8f}\n"
|
||||||
|
f"Low: {low:.8f}\n"
|
||||||
|
f"Close: {close:.8f}\n"
|
||||||
|
f"Volume: {volume:.2f}\n"
|
||||||
|
f"{price_change}\n"
|
||||||
|
"---"
|
||||||
|
)
|
||||||
|
formatted_data.append(candle_str)
|
||||||
|
|
||||||
|
return "\n".join(formatted_data)
|
||||||
|
|
||||||
|
|
||||||
@server.list_tools()
|
@server.list_tools()
|
||||||
async def handle_list_tools() -> List[types.Tool]:
|
async def handle_list_tools() -> List[types.Tool]:
|
||||||
"""List available cryptocurrency tools."""
|
"""List available cryptocurrency tools."""
|
||||||
return [
|
return [
|
||||||
|
# Market Data Tools
|
||||||
types.Tool(
|
types.Tool(
|
||||||
name="get-price",
|
name="get-price",
|
||||||
description="Get current price of a cryptocurrency pair from a specific exchange",
|
description="Get current price of a cryptocurrency pair from a specific exchange",
|
||||||
@ -116,7 +153,71 @@ async def handle_list_tools() -> List[types.Tool]:
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {}
|
"properties": {}
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
|
# Historical Data Tools
|
||||||
|
types.Tool(
|
||||||
|
name="get-historical-ohlcv",
|
||||||
|
description="Get historical OHLCV (candlestick) data for a trading pair",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"symbol": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Trading pair symbol (e.g., BTC/USDT, ETH/USDT)",
|
||||||
|
},
|
||||||
|
"timeframe": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Timeframe for candlesticks (e.g., 1m, 5m, 15m, 1h, 4h, 1d)",
|
||||||
|
"enum": ["1m", "5m", "15m", "1h", "4h", "1d"],
|
||||||
|
"default": "1h"
|
||||||
|
},
|
||||||
|
"days_back": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Number of days of historical data to fetch (default: 7, max: 30)",
|
||||||
|
"default": 7,
|
||||||
|
"maximum": 30
|
||||||
|
},
|
||||||
|
"exchange": get_exchange_schema()
|
||||||
|
},
|
||||||
|
"required": ["symbol"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
types.Tool(
|
||||||
|
name="get-price-change",
|
||||||
|
description="Get price change statistics over different time periods",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"symbol": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Trading pair symbol (e.g., BTC/USDT, ETH/USDT)",
|
||||||
|
},
|
||||||
|
"exchange": get_exchange_schema()
|
||||||
|
},
|
||||||
|
"required": ["symbol"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
types.Tool(
|
||||||
|
name="get-volume-history",
|
||||||
|
description="Get trading volume history over time",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"symbol": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Trading pair symbol (e.g., BTC/USDT, ETH/USDT)",
|
||||||
|
},
|
||||||
|
"days": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Number of days of volume history (default: 7, max: 30)",
|
||||||
|
"default": 7,
|
||||||
|
"maximum": 30
|
||||||
|
},
|
||||||
|
"exchange": get_exchange_schema()
|
||||||
|
},
|
||||||
|
"required": ["symbol"],
|
||||||
|
},
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@server.call_tool()
|
@server.call_tool()
|
||||||
@ -184,6 +285,78 @@ async def handle_call_tool(
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
elif name == "get-historical-ohlcv":
|
||||||
|
symbol = arguments.get("symbol", "").upper()
|
||||||
|
timeframe = arguments.get("timeframe", "1h")
|
||||||
|
days_back = min(int(arguments.get("days_back", 7)), 30)
|
||||||
|
|
||||||
|
# Calculate timestamps
|
||||||
|
since = int((datetime.now() - timedelta(days=days_back)).timestamp() * 1000)
|
||||||
|
|
||||||
|
# Fetch historical data
|
||||||
|
ohlcv = await exchange.fetch_ohlcv(symbol, timeframe, since=since)
|
||||||
|
|
||||||
|
formatted_data = format_ohlcv_data(ohlcv, timeframe)
|
||||||
|
return [
|
||||||
|
types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=f"Historical OHLCV data for {symbol} ({timeframe}) on {exchange_id.upper()}:\n\n{formatted_data}"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
elif name == "get-price-change":
|
||||||
|
symbol = arguments.get("symbol", "").upper()
|
||||||
|
|
||||||
|
# Get current price
|
||||||
|
ticker = await exchange.fetch_ticker(symbol)
|
||||||
|
current_price = ticker['last']
|
||||||
|
|
||||||
|
# Get historical prices
|
||||||
|
timeframes = {
|
||||||
|
"1h": (1, "1h"),
|
||||||
|
"24h": (1, "1d"),
|
||||||
|
"7d": (7, "1d"),
|
||||||
|
"30d": (30, "1d")
|
||||||
|
}
|
||||||
|
|
||||||
|
changes = []
|
||||||
|
for label, (days, timeframe) in timeframes.items():
|
||||||
|
since = int((datetime.now() - timedelta(days=days)).timestamp() * 1000)
|
||||||
|
ohlcv = await exchange.fetch_ohlcv(symbol, timeframe, since=since, limit=1)
|
||||||
|
if ohlcv:
|
||||||
|
start_price = ohlcv[0][1] # Open price
|
||||||
|
change_pct = ((current_price - start_price) / start_price) * 100
|
||||||
|
changes.append(f"{label} change: {change_pct:+.2f}%")
|
||||||
|
|
||||||
|
return [
|
||||||
|
types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=f"Price changes for {symbol} on {exchange_id.upper()}:\n\n" + "\n".join(changes)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
elif name == "get-volume-history":
|
||||||
|
symbol = arguments.get("symbol", "").upper()
|
||||||
|
days = min(int(arguments.get("days", 7)), 30)
|
||||||
|
|
||||||
|
# Get daily volume data
|
||||||
|
since = int((datetime.now() - timedelta(days=days)).timestamp() * 1000)
|
||||||
|
ohlcv = await exchange.fetch_ohlcv(symbol, "1d", since=since)
|
||||||
|
|
||||||
|
volume_data = []
|
||||||
|
for candle in ohlcv:
|
||||||
|
timestamp, _, _, _, _, volume = candle
|
||||||
|
dt = datetime.fromtimestamp(timestamp/1000).strftime('%Y-%m-%d')
|
||||||
|
volume_data.append(f"{dt}: {volume:,.2f}")
|
||||||
|
|
||||||
|
return [
|
||||||
|
types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=f"Daily trading volume history for {symbol} on {exchange_id.upper()}:\n\n" +
|
||||||
|
"\n".join(volume_data)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown tool: {name}")
|
raise ValueError(f"Unknown tool: {name}")
|
||||||
|
|
||||||
@ -201,12 +374,6 @@ async def handle_call_tool(
|
|||||||
exchange_instances.clear()
|
exchange_instances.clear()
|
||||||
|
|
||||||
|
|
||||||
def run_server():
|
|
||||||
"""Wrapper to run the async main function"""
|
|
||||||
asyncio.run(main())
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
"""Run the server using stdin/stdout streams."""
|
"""Run the server using stdin/stdout streams."""
|
||||||
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
||||||
@ -223,5 +390,11 @@ async def main():
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_server():
|
||||||
|
"""Wrapper to run the async main function"""
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user