sentrythetaai / mcp_server.py
mabdullahsibghatullah123's picture
Upload 11 files
261b0f7 verified
Raw History Blame Contribute Delete
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()