Initial commit
This commit is contained in:
commit
8d4f9eb282
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
BIN
.ropeproject/autoimport.db
Normal file
BIN
.ropeproject/autoimport.db
Normal file
Binary file not shown.
20
pyproject.toml
Normal file
20
pyproject.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[project]
|
||||
name = "mcp-server-ccxt"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = ["mcp", "ccxt"]
|
||||
|
||||
[project.scripts]
|
||||
ccxt-server = "src.server:run_server"
|
||||
|
||||
[tool.uv]
|
||||
package = true
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src"]
|
||||
7
src/__init__.py
Normal file
7
src/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
from . import server
|
||||
import asyncio
|
||||
|
||||
|
||||
|
||||
|
||||
__all__ = ['server', 'run_server']
|
||||
227
src/server.py
Normal file
227
src/server.py
Normal file
@ -0,0 +1,227 @@
|
||||
import asyncio
|
||||
from typing import Any, Dict, List
|
||||
import ccxt.async_support as ccxt
|
||||
import mcp.types as types
|
||||
from mcp.server import Server, NotificationOptions
|
||||
from mcp.server.models import InitializationOptions
|
||||
import mcp.server.stdio
|
||||
|
||||
# Initialize server
|
||||
server = Server("crypto-server")
|
||||
|
||||
# Define supported exchanges and their instances
|
||||
SUPPORTED_EXCHANGES = {
|
||||
'binance': ccxt.binance,
|
||||
'coinbase': ccxt.coinbase,
|
||||
'kraken': ccxt.kraken,
|
||||
'kucoin': ccxt.kucoin,
|
||||
'ftx': ccxt.hyperliquid,
|
||||
'huobi': ccxt.huobi,
|
||||
'bitfinex': ccxt.bitfinex,
|
||||
'bybit': ccxt.bybit,
|
||||
'okx': ccxt.okx,
|
||||
'mexc': ccxt.mexc
|
||||
}
|
||||
|
||||
# Exchange instances cache
|
||||
exchange_instances = {}
|
||||
|
||||
async def get_exchange(exchange_id: str) -> ccxt.Exchange:
|
||||
"""Get or create an exchange instance."""
|
||||
exchange_id = exchange_id.lower()
|
||||
if exchange_id not in SUPPORTED_EXCHANGES:
|
||||
raise ValueError(f"Unsupported exchange: {exchange_id}")
|
||||
|
||||
if exchange_id not in exchange_instances:
|
||||
exchange_class = SUPPORTED_EXCHANGES[exchange_id]
|
||||
exchange_instances[exchange_id] = exchange_class()
|
||||
|
||||
return exchange_instances[exchange_id]
|
||||
|
||||
async def format_ticker(ticker: Dict[str, Any], exchange_id: str) -> str:
|
||||
"""Format ticker data into a readable string."""
|
||||
return (
|
||||
f"Exchange: {exchange_id.upper()}\n"
|
||||
f"Symbol: {ticker.get('symbol')}\n"
|
||||
f"Last Price: {ticker.get('last', 'N/A')}\n"
|
||||
f"24h High: {ticker.get('high', 'N/A')}\n"
|
||||
f"24h Low: {ticker.get('low', 'N/A')}\n"
|
||||
f"24h Volume: {ticker.get('baseVolume', 'N/A')}\n"
|
||||
f"Bid: {ticker.get('bid', 'N/A')}\n"
|
||||
f"Ask: {ticker.get('ask', 'N/A')}\n"
|
||||
"---"
|
||||
)
|
||||
|
||||
def get_exchange_schema() -> Dict[str, Any]:
|
||||
"""Get the JSON schema for exchange selection."""
|
||||
return {
|
||||
"type": "string",
|
||||
"description": f"Exchange to use (supported: {', '.join(SUPPORTED_EXCHANGES.keys())})",
|
||||
"enum": list(SUPPORTED_EXCHANGES.keys()),
|
||||
"default": "binance"
|
||||
}
|
||||
|
||||
@server.list_tools()
|
||||
async def handle_list_tools() -> List[types.Tool]:
|
||||
"""List available cryptocurrency tools."""
|
||||
return [
|
||||
types.Tool(
|
||||
name="get-price",
|
||||
description="Get current price of a cryptocurrency pair from a specific exchange",
|
||||
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-market-summary",
|
||||
description="Get detailed market summary for a cryptocurrency pair from a specific exchange",
|
||||
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-top-volumes",
|
||||
description="Get top cryptocurrencies by trading volume from a specific exchange",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "number",
|
||||
"description": "Number of pairs to return (default: 5)",
|
||||
},
|
||||
"exchange": get_exchange_schema()
|
||||
}
|
||||
},
|
||||
),
|
||||
types.Tool(
|
||||
name="list-exchanges",
|
||||
description="List all supported cryptocurrency exchanges",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
@server.call_tool()
|
||||
async def handle_call_tool(
|
||||
name: str, arguments: Dict[str, Any]
|
||||
) -> List[types.TextContent]:
|
||||
"""Handle tool execution requests."""
|
||||
try:
|
||||
if name == "list-exchanges":
|
||||
exchange_list = "\n".join([f"- {ex.upper()}" for ex in SUPPORTED_EXCHANGES.keys()])
|
||||
return [
|
||||
types.TextContent(
|
||||
type="text",
|
||||
text=f"Supported exchanges:\n\n{exchange_list}"
|
||||
)
|
||||
]
|
||||
|
||||
# Get exchange from arguments or use default
|
||||
exchange_id = arguments.get("exchange", "binance")
|
||||
exchange = await get_exchange(exchange_id)
|
||||
|
||||
if name == "get-price":
|
||||
symbol = arguments.get("symbol", "").upper()
|
||||
ticker = await exchange.fetch_ticker(symbol)
|
||||
|
||||
return [
|
||||
types.TextContent(
|
||||
type="text",
|
||||
text=f"Current price of {symbol} on {exchange_id.upper()}: {ticker['last']} {symbol.split('/')[1]}"
|
||||
)
|
||||
]
|
||||
|
||||
elif name == "get-market-summary":
|
||||
symbol = arguments.get("symbol", "").upper()
|
||||
ticker = await exchange.fetch_ticker(symbol)
|
||||
|
||||
formatted_data = await format_ticker(ticker, exchange_id)
|
||||
return [
|
||||
types.TextContent(
|
||||
type="text",
|
||||
text=f"Market summary for {symbol}:\n\n{formatted_data}"
|
||||
)
|
||||
]
|
||||
|
||||
elif name == "get-top-volumes":
|
||||
limit = int(arguments.get("limit", 5))
|
||||
tickers = await exchange.fetch_tickers()
|
||||
|
||||
# Sort by volume and get top N
|
||||
sorted_tickers = sorted(
|
||||
tickers.values(),
|
||||
key=lambda x: float(x.get('baseVolume', 0) or 0),
|
||||
reverse=True
|
||||
)[:limit]
|
||||
|
||||
formatted_results = []
|
||||
for ticker in sorted_tickers:
|
||||
formatted_data = await format_ticker(ticker, exchange_id)
|
||||
formatted_results.append(formatted_data)
|
||||
|
||||
return [
|
||||
types.TextContent(
|
||||
type="text",
|
||||
text=f"Top {limit} pairs by volume on {exchange_id.upper()}:\n\n" + "\n".join(formatted_results)
|
||||
)
|
||||
]
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
except ccxt.BaseError as e:
|
||||
return [
|
||||
types.TextContent(
|
||||
type="text",
|
||||
text=f"Error accessing cryptocurrency data: {str(e)}"
|
||||
)
|
||||
]
|
||||
finally:
|
||||
# Clean up exchange connections
|
||||
for instance in exchange_instances.values():
|
||||
await instance.close()
|
||||
exchange_instances.clear()
|
||||
|
||||
|
||||
def run_server():
|
||||
"""Wrapper to run the async main function"""
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run the server using stdin/stdout streams."""
|
||||
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
||||
await server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
InitializationOptions(
|
||||
server_name="crypto-server",
|
||||
server_version="0.1.0",
|
||||
capabilities=server.get_capabilities(
|
||||
notification_options=NotificationOptions(),
|
||||
experimental_capabilities={},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Loading…
Reference in New Issue
Block a user