""" Gridbot MCP Server - Wraps eToroGridbot REST API as MCP tools. Requires SSH tunnel: ssh -L 8000:localhost:8000 services """ import os import json from typing import Any import httpx from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent BASE_URL = os.getenv("GRIDBOT_API_URL", "http://localhost:8000") TIMEOUT = 30.0 server = Server("gridbot-mcp") async def api_get(endpoint: str) -> dict[str, Any]: """Make GET request to gridbot API.""" async with httpx.AsyncClient(timeout=TIMEOUT) as client: try: resp = await client.get(f"{BASE_URL}{endpoint}") resp.raise_for_status() return resp.json() except httpx.ConnectError: return {"error": "Connection failed - is SSH tunnel running?"} except httpx.HTTPStatusError as e: return {"error": f"HTTP {e.response.status_code}: {e.response.text}"} except Exception as e: return {"error": str(e)} async def api_post(endpoint: str, data: dict[str, Any] | None = None) -> dict[str, Any]: """Make POST request to gridbot API.""" async with httpx.AsyncClient(timeout=TIMEOUT) as client: try: resp = await client.post(f"{BASE_URL}{endpoint}", json=data or {}) resp.raise_for_status() return resp.json() except httpx.ConnectError: return {"error": "Connection failed - is SSH tunnel running?"} except httpx.HTTPStatusError as e: return {"error": f"HTTP {e.response.status_code}: {e.response.text}"} except Exception as e: return {"error": str(e)} async def api_delete(endpoint: str) -> dict[str, Any]: """Make DELETE request to gridbot API.""" async with httpx.AsyncClient(timeout=TIMEOUT) as client: try: resp = await client.delete(f"{BASE_URL}{endpoint}") resp.raise_for_status() return resp.json() except httpx.ConnectError: return {"error": "Connection failed - is SSH tunnel running?"} except httpx.HTTPStatusError as e: return {"error": f"HTTP {e.response.status_code}: {e.response.text}"} except Exception as e: return {"error": str(e)} @server.list_tools() async def list_tools() -> list[Tool]: """List available gridbot tools.""" return [ Tool( name="health", description="Check gridbot conductor health status, queue size, and capacity", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="emergency_status", description="Get emergency stop status, circuit breaker state, drawdown, and daily loss limits", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="market_hours", description="Get market hours summary for all exchanges (NYSE, NASDAQ, CRYPTO, FOREX, etc)", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="positions", description="Get all portfolio positions with real-time P/L (uses BotPriceProvider, works on weekends)", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="pending_orders", description="Get all pending limit orders with amounts, rates, TP/SL, and headroom", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="risk_score", description="Get current portfolio risk score and components (volatility, drawdown, concentration, leverage)", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="vix", description="Get VIX (volatility index) value and interpretation (LOW/NORMAL/ELEVATED/HIGH)", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="crypto_fear_greed", description="Get Crypto Fear & Greed index with classification and trading guidance", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="dashboard", description="Get full dashboard summary including positions, orders, and analytics", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="analytics", description="Get trading analytics summary (win rate, Sharpe ratio, P/L)", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="validate_trade", description="Validate a trade before execution (checks risk limits, concentration, etc)", inputSchema={ "type": "object", "properties": { "symbol": {"type": "string", "description": "Ticker symbol (e.g., BTC, AAPL)"}, "side": {"type": "string", "enum": ["BUY", "SELL"], "description": "Trade direction"}, "amount": {"type": "number", "description": "Trade amount in USD"}, }, "required": ["symbol", "side", "amount"], }, ), Tool( name="queue_status", description="Get signal processing queue status", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="emergency_stop", description="HALT all trading (emergency use only)", inputSchema={ "type": "object", "properties": { "reason": {"type": "string", "description": "Reason for emergency stop"}, "confirm": {"type": "boolean", "description": "Must be true to execute"}, }, "required": ["reason", "confirm"], }, ), Tool( name="emergency_reset", description="Resume trading after emergency stop", inputSchema={ "type": "object", "properties": { "confirm": {"type": "boolean", "description": "Must be true to execute"}, }, "required": ["confirm"], }, ), Tool( name="cleanup_stale_orders", description="Cleanup stale/expired pending orders", inputSchema={ "type": "object", "properties": { "confirm": {"type": "boolean", "description": "Must be true to execute"}, }, "required": ["confirm"], }, ), # === NEW TOOLS === Tool( name="bot_health", description="Get health status of all trading bots (Freqtrade, Hummingbot, etc)", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="stale_bots", description="Get list of bots that haven't sent signals recently", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="correlations", description="Get top correlated asset pairs in portfolio", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="trades", description="Get recent trades with P/L details", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="expectancy", description="Get trading expectancy metrics for all symbols", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="profitable_symbols", description="Get list of profitable symbols ranked by expectancy", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="monitoring_stats", description="Get system monitoring stats (signal counts, rejection reasons, latency)", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="statistics", description="Get trading statistics summary (win rate, avg P/L, trade counts)", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="support_resistance", description="Get support/resistance levels for a symbol", inputSchema={ "type": "object", "properties": { "symbol": {"type": "string", "description": "Ticker symbol (e.g., BTC, AAPL)"}, }, "required": ["symbol"], }, ), Tool( name="signal_quality", description="Get signal quality report (acceptance rate, rejection reasons)", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="trend_alerts", description="Get active trend change alerts for monitored positions", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="env_health", description="Get environment health (API keys, credentials status)", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="candle_freshness", description="Get candle data freshness (staleness check for each instrument)", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="rate_limiter", description="Get rate limiter statistics (requests per minute, throttled count)", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="portfolio_allocation", description="Get portfolio allocation breakdown (crypto %, stocks %, metals %)", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="create_order", description="Create a new order (BUY/SELL) with full validation pipeline. Goes through all risk checks.", inputSchema={ "type": "object", "properties": { "symbol": {"type": "string", "description": "Trading symbol (e.g., GLD, NVDA, BTC)"}, "action": {"type": "string", "enum": ["BUY", "SELL"], "description": "Trade direction"}, "amount": {"type": "number", "description": "Position size in USD (max 10000)"}, "order_type": {"type": "string", "enum": ["MARKET", "LIMIT"], "default": "LIMIT", "description": "Order type"}, "limit_price": {"type": "number", "description": "Limit price (required for LIMIT orders, omit for MARKET)"}, "take_profit_pct": {"type": "number", "description": "Take profit percentage (e.g., 10 for 10%)"}, "stop_loss_pct": {"type": "number", "description": "Stop loss percentage (e.g., 5 for 5%)"}, "dry_run": {"type": "boolean", "default": False, "description": "Validate only, do not execute"}, }, "required": ["symbol", "action", "amount"], }, ), Tool( name="close_position", description="Close an existing position (full or partial). Use position_id from positions tool.", inputSchema={ "type": "object", "properties": { "position_id": {"type": "integer", "description": "eToro position ID to close"}, "close_percentage": {"type": "number", "description": "Percentage to close (1-100). Omit for full close."}, }, "required": ["position_id"], }, ), # === SIGNAL INTENTS === Tool( name="intents", description="List signal intents (pending orders awaiting trigger price). Filter by status or symbol.", inputSchema={ "type": "object", "properties": { "status": {"type": "string", "enum": ["pending", "executed", "expired", "rejected"], "description": "Filter by status"}, "symbol": {"type": "string", "description": "Filter by symbol (e.g., BTC, AAPL)"}, "limit": {"type": "number", "default": 50, "description": "Max results (1-200)"}, }, }, ), Tool( name="intents_pending", description="List pending signal intents awaiting execution (shortcut for intents with status=pending)", inputSchema={ "type": "object", "properties": { "symbol": {"type": "string", "description": "Filter by symbol (e.g., BTC, AAPL)"}, "limit": {"type": "number", "default": 50, "description": "Max results (1-200)"}, }, }, ), Tool( name="intents_executed", description="List executed signal intents (orders that were triggered and filled)", inputSchema={ "type": "object", "properties": { "symbol": {"type": "string", "description": "Filter by symbol (e.g., BTC, AAPL)"}, "limit": {"type": "number", "default": 50, "description": "Max results (1-200)"}, }, }, ), Tool( name="intents_stats", description="Get signal intent statistics (pending/executed/expired counts, execution rate, amounts)", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name="create_intent", description="Create a signal intent (limit order that executes when price reaches trigger). Use for weekend/after-hours trading.", inputSchema={ "type": "object", "properties": { "symbol": {"type": "string", "description": "Trading symbol (e.g., BTC, NVDA, COIN)"}, "is_buy": {"type": "boolean", "default": True, "description": "True for BUY, False for SELL"}, "amount": {"type": "number", "description": "Position size in USD (max 10000)"}, "trigger_price": {"type": "number", "description": "Price at which to trigger execution"}, "take_profit_pct": {"type": "number", "description": "Take profit % (e.g., 10 for 10%). Default: 15% crypto, 9% stocks"}, "stop_loss_pct": {"type": "number", "description": "Stop loss % (e.g., 5 for 5%). Default: 10% crypto, 6% stocks"}, "expires_in_hours": {"type": "number", "description": "Hours until expiration (default: 168 stocks, 72 crypto)"}, "source_type": {"type": "string", "default": "manual", "description": "Signal source: manual, freqtrade, hummingbot, pi_post, newsletter"}, }, "required": ["symbol", "amount", "trigger_price"], }, ), Tool( name="cancel_intent", description="Cancel a pending signal intent by ID", inputSchema={ "type": "object", "properties": { "intent_id": {"type": "string", "description": "Intent ID to cancel"}, }, "required": ["intent_id"], }, ), # === ORDER MANAGEMENT === Tool( name="cancel_order", description="Cancel a single pending limit order by order_id", inputSchema={ "type": "object", "properties": { "order_id": {"type": "integer", "description": "Order ID to cancel (from pending_orders)"}, }, "required": ["order_id"], }, ), Tool( name="cancel_orders_bulk", description="Cancel multiple pending orders at once. Use for cleaning up duplicates.", inputSchema={ "type": "object", "properties": { "order_ids": { "type": "array", "items": {"type": "integer"}, "description": "List of order IDs to cancel", }, "confirm": {"type": "boolean", "description": "Must be true to execute"}, }, "required": ["order_ids", "confirm"], }, ), Tool( name="find_duplicate_orders", description="Find duplicate pending orders (same instrument + rate + direction). Returns order IDs to cancel.", inputSchema={"type": "object", "properties": {}, "required": []}, ), # === CONTAINER OPERATIONS === Tool( name="container_logs", description="Get container logs with optional grep filter. Allowed: conductor, freqtrade, hummingbot, dashboard, postgres, redis.", inputSchema={ "type": "object", "properties": { "container": {"type": "string", "enum": ["conductor", "freqtrade", "hummingbot", "dashboard", "postgres", "redis"], "description": "Container name"}, "tail": {"type": "number", "default": 100, "description": "Number of lines (1-1000)"}, "grep": {"type": "string", "description": "Grep pattern to filter logs"}, "since": {"type": "string", "description": "Show logs since (e.g., '1h', '30m')"}, }, "required": ["container"], }, ), Tool( name="container_restart", description="Restart a container (requires confirm=true). Allowed: conductor, freqtrade, hummingbot, dashboard, postgres, redis.", inputSchema={ "type": "object", "properties": { "container": {"type": "string", "enum": ["conductor", "freqtrade", "hummingbot", "dashboard", "postgres", "redis"], "description": "Container to restart"}, "confirm": {"type": "boolean", "description": "Must be true to execute"}, }, "required": ["container", "confirm"], }, ), ] @server.call_tool() async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]: """Execute a gridbot tool.""" result: dict[str, Any] match name: case "health": result = await api_get("/health") case "emergency_status": result = await api_get("/emergency/status") case "market_hours": result = await api_get("/api/market-hours/summary") case "positions": result = await api_get("/portfolio/positions/detailed") case "pending_orders": result = await api_get("/portfolio/pending-orders") case "risk_score": result = await api_get("/api/risk-score/latest") case "vix": result = await api_get("/api/market-intel/vix") case "crypto_fear_greed": result = await api_get("/api/market-intel/crypto-fear-greed") case "dashboard": result = await api_get("/api/dashboard/summary") case "analytics": result = await api_get("/api/analytics/summary") case "queue_status": result = await api_get("/queue/status") case "validate_trade": result = await api_post("/portfolio/validate-trade", { "symbol": arguments["symbol"], "side": arguments["side"], "amount": arguments["amount"], }) case "emergency_stop": if not arguments.get("confirm"): result = {"error": "Must set confirm=true to execute emergency stop"} else: result = await api_post("/emergency/stop", { "reason": arguments.get("reason", "Manual halt"), "confirmation": True, }) case "emergency_reset": if not arguments.get("confirm"): result = {"error": "Must set confirm=true to reset emergency stop"} else: result = await api_post("/emergency/reset", {"confirmation": True}) case "cleanup_stale_orders": if not arguments.get("confirm"): result = {"error": "Must set confirm=true to cleanup orders"} else: result = await api_post("/api/order-ttl/cleanup", {"confirmation": True}) # === NEW TOOL HANDLERS === case "bot_health": result = await api_get("/api/bots/health") case "stale_bots": result = await api_get("/api/bots/stale") case "correlations": result = await api_get("/api/correlations/top") case "trades": result = await api_get("/api/dashboard/trades") case "expectancy": result = await api_get("/api/expectancy/") case "profitable_symbols": result = await api_get("/api/expectancy/profitable/list") case "monitoring_stats": result = await api_get("/api/monitoring/stats") case "statistics": result = await api_get("/api/statistics/summary") case "support_resistance": symbol = arguments.get("symbol", "BTC") result = await api_get(f"/api/support-resistance/levels?symbol={symbol}") case "signal_quality": result = await api_get("/api/signal-quality/report") case "trend_alerts": result = await api_get("/api/trend-monitoring/alerts") case "env_health": result = await api_get("/api/health/env") case "candle_freshness": result = await api_get("/api/monitoring/candle-cache/stats") case "rate_limiter": result = await api_get("/api/monitoring/rate_limiter/stats") case "portfolio_allocation": result = await api_get("/api/portfolio/status") case "create_order": payload = { "symbol": arguments["symbol"], "action": arguments["action"], "amount": arguments["amount"], "order_type": arguments.get("order_type", "LIMIT"), "take_profit_pct": arguments.get("take_profit_pct", 10), "stop_loss_pct": arguments.get("stop_loss_pct", 5), "dry_run": arguments.get("dry_run", False), } if arguments.get("limit_price"): payload["limit_price"] = arguments["limit_price"] result = await api_post("/api/trade/order", payload) case "close_position": payload = {"position_id": arguments["position_id"]} if arguments.get("close_percentage"): payload["close_percentage"] = arguments["close_percentage"] result = await api_post("/api/trade/close", payload) # === SIGNAL INTENTS === case "intents": params = [] if arguments.get("status"): params.append(f"status={arguments['status']}") if arguments.get("symbol"): params.append(f"symbol={arguments['symbol']}") if arguments.get("limit"): params.append(f"limit={arguments['limit']}") query = f"?{'&'.join(params)}" if params else "" result = await api_get(f"/intents{query}") case "intents_pending": params = [] if arguments.get("symbol"): params.append(f"symbol={arguments['symbol']}") if arguments.get("limit"): params.append(f"limit={arguments['limit']}") query = f"?{'&'.join(params)}" if params else "" result = await api_get(f"/intents/pending{query}") case "intents_executed": params = [] if arguments.get("symbol"): params.append(f"symbol={arguments['symbol']}") if arguments.get("limit"): params.append(f"limit={arguments['limit']}") query = f"?{'&'.join(params)}" if params else "" result = await api_get(f"/intents/executed{query}") case "intents_stats": result = await api_get("/intents/stats") case "create_intent": payload = { "symbol": arguments["symbol"], "is_buy": arguments.get("is_buy", True), "amount": arguments["amount"], "trigger_price": arguments["trigger_price"], } if arguments.get("take_profit_pct"): payload["take_profit_pct"] = arguments["take_profit_pct"] if arguments.get("stop_loss_pct"): payload["stop_loss_pct"] = arguments["stop_loss_pct"] if arguments.get("expires_in_hours"): payload["expires_in_hours"] = arguments["expires_in_hours"] if arguments.get("source_type"): payload["source_type"] = arguments["source_type"] result = await api_post("/intents", payload) case "cancel_intent": intent_id = arguments["intent_id"] result = await api_delete(f"/intents/{intent_id}") # === ORDER MANAGEMENT === case "cancel_order": order_id = arguments["order_id"] result = await api_delete(f"/api/trade/order/{order_id}") case "cancel_orders_bulk": if not arguments.get("confirm"): result = {"error": "Must set confirm=true to cancel orders"} else: order_ids = arguments["order_ids"] result = await api_post("/api/trade/orders/cancel-bulk", {"order_ids": order_ids}) case "find_duplicate_orders": result = await api_get("/api/trade/orders/duplicates") # === CONTAINER OPERATIONS === case "container_logs": params = [f"container={arguments['container']}"] if arguments.get("tail"): params.append(f"tail={arguments['tail']}") if arguments.get("grep"): params.append(f"grep={arguments['grep']}") if arguments.get("since"): params.append(f"since={arguments['since']}") result = await api_get(f"/containers/logs?{'&'.join(params)}") case "container_restart": result = await api_post( f"/containers/restart?container={arguments['container']}&confirm={str(arguments.get('confirm', False)).lower()}" ) case _: result = {"error": f"Unknown tool: {name}"} return [TextContent(type="text", text=json.dumps(result, indent=2))] async def main(): """Run the MCP server.""" async with stdio_server() as (read_stream, write_stream): await server.run(read_stream, write_stream, server.create_initialization_options()) if __name__ == "__main__": import asyncio asyncio.run(main())