Spaces:
Running
Running
Download mcp_server.py from sysvelop/sentrythetaai: direct link, hf CLI and curl.
- Browser
- Download file 12.5 kB
-
https://huggingface.co/spaces/sysvelop/sentrythetaai/resolve/main/mcp_server.py
- Command line
-
hf download hf://spaces/sysvelop/sentrythetaai/mcp_server.py
-
curl -L -o mcp_server.py https://huggingface.co/spaces/sysvelop/sentrythetaai/resolve/main/mcp_server.py
12.5 kB
| import sys | |
| import json | |
| import logging | |
| from typing import Dict, Any, List | |
| # Load environment | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| from backend.alpaca_helper import AlpacaHelper | |
| from backend.risk_engine import RiskEngine | |
| from backend.agent import SentryThetaAgent | |
| # Configure basic logging to stderr so it doesn't corrupt stdout (which is used for JSON-RPC messages) | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", | |
| stream=sys.stderr | |
| ) | |
| logger = logging.getLogger("SentryTheta.MCP") | |
| # Initialize backends | |
| alpaca = AlpacaHelper() | |
| risk_engine = RiskEngine() | |
| agent = SentryThetaAgent(alpaca, risk_engine) | |
| def get_tools_list() -> List[Dict[str, Any]]: | |
| return [ | |
| { | |
| "name": "get_portfolio_status", | |
| "description": "Fetch portfolio metrics (equity, cash, P&L) and current active options positions.", | |
| "inputSchema": { | |
| "type": "object", | |
| "properties": {} | |
| } | |
| }, | |
| { | |
| "name": "analyze_and_propose_trade", | |
| "description": "Analyze sentiment for a target stock and search options chains to propose a risk-checked option trade strategy.", | |
| "inputSchema": { | |
| "type": "object", | |
| "properties": { | |
| "ticker": { | |
| "type": "string", | |
| "description": "The stock ticker symbol (e.g. AAPL, MSFT, NVDA, SPY, QQQ)" | |
| } | |
| }, | |
| "required": ["ticker"] | |
| } | |
| }, | |
| { | |
| "name": "execute_option_order", | |
| "description": "Directly place an option contract trade order (buy/sell). Evaluates risk gates first.", | |
| "inputSchema": { | |
| "type": "object", | |
| "properties": { | |
| "symbol": { | |
| "type": "string", | |
| "description": "The OCC Option Symbol (e.g., AAPL260904C00220000)" | |
| }, | |
| "qty": { | |
| "type": "integer", | |
| "description": "Quantity of contracts to trade (each contract represents 100 shares)" | |
| }, | |
| "side": { | |
| "type": "string", | |
| "enum": ["buy", "sell"], | |
| "description": "Order side (buy to open/close or sell to open/close)" | |
| }, | |
| "price": { | |
| "type": "number", | |
| "description": "Approximate current premium price per contract (for risk engine evaluation)" | |
| } | |
| }, | |
| "required": ["symbol", "qty", "side", "price"] | |
| } | |
| }, | |
| { | |
| "name": "update_sentry_settings", | |
| "description": "Dynamically adjust options strategy risk engine thresholds.", | |
| "inputSchema": { | |
| "type": "object", | |
| "properties": { | |
| "max_position_size_pct": { | |
| "type": "number", | |
| "description": "Maximum size of a single trade as percentage of equity" | |
| }, | |
| "max_daily_drawdown_pct": { | |
| "type": "number", | |
| "description": "Maximum daily loss percentage threshold before trading is blocked" | |
| }, | |
| "stop_loss_pct": { | |
| "type": "number", | |
| "description": "Percentage loss threshold to auto-liquidate active positions" | |
| }, | |
| "take_profit_pct": { | |
| "type": "number", | |
| "description": "Percentage gain target to auto-harvest profits" | |
| } | |
| } | |
| } | |
| } | |
| ] | |
| def handle_tool_call(name: str, arguments: Dict[str, Any]) -> Dict[str, Any]: | |
| logger.info(f"Handling tool call: {name} with args {arguments}") | |
| if name == "get_portfolio_status": | |
| acc = alpaca.get_account_info() | |
| positions = alpaca.get_active_positions() | |
| status_txt = ( | |
| f"=== Portfolio Status (Mock Mode: {acc['is_mock']}) ===\n" | |
| f"Total Equity: ${acc['equity']:.2f}\n" | |
| f"Cash Balance: ${acc['cash']:.2f}\n" | |
| f"Buying Power: ${acc['buying_power']:.2f}\n" | |
| f"Today's P&L: ${acc['today_pl']:.2f} ({acc['today_plpc']*100:+.2f}%)\n\n" | |
| f"Active Options Contracts ({len(positions)}):\n" | |
| ) | |
| if not positions: | |
| status_txt += "- No active options positions." | |
| for pos in positions: | |
| status_txt += ( | |
| f"- {pos['symbol']}: {pos['side'].upper()} {pos['qty']}x {pos['type'].upper()} " | |
| f"Entry: ${pos['entry_price']:.2f} | Current: ${pos['current_price']:.2f} | " | |
| f"P&L: {pos['unrealized_pl']:+.2f} ({pos['unrealized_plpc']*100:+.1f}%)\n" | |
| ) | |
| return { | |
| "content": [{"type": "text", "text": status_txt}] | |
| } | |
| elif name == "analyze_and_propose_trade": | |
| ticker = arguments.get("ticker", "").upper().strip() | |
| if not ticker: | |
| return {"content": [{"type": "text", "text": "Error: Ticker symbol is required."}]} | |
| sentiment, analysis = agent.analyze_market_sentiment(ticker) | |
| proposal = agent.propose_options_trade(ticker, sentiment) | |
| result_txt = ( | |
| f"=== Market Analysis for {ticker} ===\n" | |
| f"Market Sentiment Score: {sentiment:+.2f}\n" | |
| f"LLM Sentiment Driver: {analysis}\n\n" | |
| ) | |
| if proposal: | |
| result_txt += ( | |
| f"=== Proposed Option Strategy ===\n" | |
| f"Strategy: {proposal['strategy']}\n" | |
| f"Contract Symbol: {proposal['symbol']}\n" | |
| f"Leg action: {proposal['side'].upper()} {proposal['qty']} contract(s)\n" | |
| f"Strike Price: ${proposal['strike']}\n" | |
| f"Expiration Date: {proposal['expiration']}\n" | |
| f"Est. Premium per Contract: ${proposal['premium']:.2f}\n" | |
| f"Total Capital Committed: ${proposal['total_value']:.2f}\n\n" | |
| ) | |
| # Check Risk | |
| acc = alpaca.get_account_info() | |
| approved, risk_reason = risk_engine.evaluate_trade( | |
| symbol=proposal["symbol"], | |
| qty=proposal["qty"], | |
| premium_per_contract=proposal["premium"], | |
| side=proposal["side"], | |
| account_info=acc | |
| ) | |
| result_txt += ( | |
| f"=== Sentry Risk Assessment ===\n" | |
| f"Status: {'APPROVED' if approved else 'REJECTED'}\n" | |
| f"Assessment Detail: {risk_reason}\n" | |
| ) | |
| else: | |
| result_txt += f"=== Proposed Option Strategy ===\nNo viable option strategy found in current chain for {ticker}." | |
| return { | |
| "content": [{"type": "text", "text": result_txt}] | |
| } | |
| elif name == "execute_option_order": | |
| symbol = arguments.get("symbol") | |
| qty = arguments.get("qty") | |
| side = arguments.get("side") | |
| price = arguments.get("price") | |
| # Check risk | |
| acc = alpaca.get_account_info() | |
| approved, risk_reason = risk_engine.evaluate_trade( | |
| symbol=symbol, | |
| qty=qty, | |
| premium_per_contract=price, | |
| side=side, | |
| account_info=acc | |
| ) | |
| if not approved: | |
| return { | |
| "content": [{"type": "text", "text": f"Execution Blocked by Risk Sentry. Reason: {risk_reason}"}] | |
| } | |
| # Execute | |
| res = alpaca.execute_options_trade( | |
| symbol=symbol, | |
| qty=qty, | |
| side=side, | |
| strategy_name="MCP Manual Order" | |
| ) | |
| if res["success"]: | |
| msg = ( | |
| f"Order successfully transmitted and filled!\n" | |
| f"Order ID: {res['order_id']}\n" | |
| f"Contract: {res['symbol']}\n" | |
| f"Action: {res['side'].upper()} {res['qty']} contract(s) filled (Status: {res['status']})." | |
| ) | |
| else: | |
| msg = f"Order execution failed on Alpaca: {res.get('error')}" | |
| return { | |
| "content": [{"type": "text", "text": msg}] | |
| } | |
| elif name == "update_sentry_settings": | |
| if "max_position_size_pct" in arguments: | |
| risk_engine.max_position_size_pct = float(arguments["max_position_size_pct"]) | |
| if "max_daily_drawdown_pct" in arguments: | |
| risk_engine.max_daily_drawdown_pct = float(arguments["max_daily_drawdown_pct"]) | |
| if "stop_loss_pct" in arguments: | |
| risk_engine.stop_loss_pct = float(arguments["stop_loss_pct"]) | |
| if "take_profit_pct" in arguments: | |
| risk_engine.take_profit_pct = float(arguments["take_profit_pct"]) | |
| msg = ( | |
| f"Risk settings updated successfully:\n" | |
| f"- Max Pos Size: {risk_engine.max_position_size_pct}%\n" | |
| f"- Max Daily Drawdown: {risk_engine.max_daily_drawdown_pct}%\n" | |
| f"- Stop Loss: {risk_engine.stop_loss_pct}%\n" | |
| f"- Take Profit: {risk_engine.take_profit_pct}%" | |
| ) | |
| return { | |
| "content": [{"type": "text", "text": msg}] | |
| } | |
| else: | |
| return {"content": [{"type": "text", "text": f"Error: Tool '{name}' not found."}]} | |
| def main(): | |
| logger.info("SentryTheta MCP Stdio Server started.") | |
| for line in sys.stdin: | |
| if not line.strip(): | |
| continue | |
| try: | |
| request = json.loads(line) | |
| # Ensure JSON-RPC protocol | |
| if "jsonrpc" not in request: | |
| continue | |
| req_id = request.get("id") | |
| method = request.get("method") | |
| # 1. Initialize | |
| if method == "initialize": | |
| response = { | |
| "jsonrpc": "2.0", | |
| "id": req_id, | |
| "result": { | |
| "protocolVersion": "2024-11-05", | |
| "capabilities": { | |
| "tools": {} | |
| }, | |
| "serverInfo": { | |
| "name": "sentrytheta-mcp", | |
| "version": "1.0.0" | |
| } | |
| } | |
| } | |
| # 2. List tools | |
| elif method == "tools/list": | |
| response = { | |
| "jsonrpc": "2.0", | |
| "id": req_id, | |
| "result": { | |
| "tools": get_tools_list() | |
| } | |
| } | |
| # 3. Call tool | |
| elif method == "tools/call": | |
| params = request.get("params", {}) | |
| tool_name = params.get("name") | |
| tool_args = params.get("arguments", {}) | |
| result = handle_tool_call(tool_name, tool_args) | |
| response = { | |
| "jsonrpc": "2.0", | |
| "id": req_id, | |
| "result": result | |
| } | |
| else: | |
| response = { | |
| "jsonrpc": "2.0", | |
| "id": req_id, | |
| "error": { | |
| "code": -32601, | |
| "message": f"Method '{method}' not found" | |
| } | |
| } | |
| # Send response to stdout | |
| sys.stdout.write(json.dumps(response) + "\n") | |
| sys.stdout.flush() | |
| except Exception as e: | |
| logger.error(f"Error handling JSON-RPC message: {e}") | |
| try: | |
| # Attempt to return general parse error | |
| sys.stdout.write(json.dumps({ | |
| "jsonrpc": "2.0", | |
| "error": { | |
| "code": -32700, | |
| "message": f"Parse error or execution failure: {str(e)}" | |
| } | |
| }) + "\n") | |
| sys.stdout.flush() | |
| except: | |
| pass | |
| if __name__ == "__main__": | |
| main() | |