""" MCP Client for HERMES Connects to local or remote MCP servers (Modal deployment) """ import os import json from typing import Dict, List, Optional, Any import modal # Modal app reference try: # Connect to deployed Modal app stub = modal.Stub.lookup("hermes-astrology-mcp", create_if_missing=False) MODAL_AVAILABLE = True except Exception as e: print(f"Modal app not found: {e}") MODAL_AVAILABLE = False class MCPClient: """Client for interacting with HERMES MCP servers""" def __init__(self, mode: str = "local"): """ Initialize MCP client Args: mode: "local" for local functions, "modal" for Modal serverless """ self.mode = mode if mode == "modal" and not MODAL_AVAILABLE: print("⚠️ Modal not available, falling back to local mode") self.mode = "local" def get_bound_ruler(self, sign: str, degree: float) -> Dict[str, Any]: """Get Egyptian bound (term) ruler for a position""" if self.mode == "modal": try: func = stub.get_bound_ruler return func.remote(sign, degree) except Exception as e: print(f"Modal error: {e}, falling back to local") return self._local_get_bound_ruler(sign, degree) else: return self._local_get_bound_ruler(sign, degree) def _local_get_bound_ruler(self, sign: str, degree: float) -> Dict[str, Any]: """Local implementation of bound ruler""" from app import EGYPTIAN_BOUNDS if sign not in EGYPTIAN_BOUNDS: return {"error": f"Invalid sign: {sign}"} bounds = EGYPTIAN_BOUNDS[sign] for start, end, ruler in bounds: if start <= degree < end: return { "sign": sign, "degree": degree, "bound_ruler": ruler, "bound_range": f"{start}-{end}°", "dignity_points": 2 } return {"error": "Degree out of range"} def get_decan_ruler(self, sign: str, degree: float) -> Dict[str, Any]: """Get decan (face) ruler for a position""" if self.mode == "modal": try: func = stub.get_decan_ruler return func.remote(sign, degree) except Exception as e: print(f"Modal error: {e}, falling back to local") return self._local_get_decan_ruler(sign, degree) else: return self._local_get_decan_ruler(sign, degree) def _local_get_decan_ruler(self, sign: str, degree: float) -> Dict[str, Any]: """Local implementation of decan ruler""" from app import DECANS if sign not in DECANS: return {"error": f"Invalid sign: {sign}"} decans = DECANS[sign] for start, end, ruler in decans: if start <= degree < end: return { "sign": sign, "degree": degree, "decan_ruler": ruler, "decan_number": (start // 10) + 1, "decan_range": f"{start}-{end}°", "dignity_points": 1 } return {"error": "Degree out of range"} def calculate_zodiacal_releasing( self, starting_sign: str, birth_date: str, calculation_date: Optional[str] = None ) -> Dict[str, Any]: """Calculate Zodiacal Releasing periods""" if self.mode == "modal": try: func = stub.calculate_zodiacal_releasing return func.remote(starting_sign, birth_date, calculation_date) except Exception as e: print(f"Modal error: {e}, falling back to local") return {"error": f"Local ZR not implemented: {e}"} else: return {"error": "Zodiacal Releasing only available via Modal server"} def calculate_firdaria( self, birth_date: str, is_day_chart: bool, years_to_calculate: int = 75 ) -> Dict[str, Any]: """Calculate Firdaria (Persian time-lord) periods""" if self.mode == "modal": try: func = stub.calculate_firdaria return func.remote(birth_date, is_day_chart, years_to_calculate) except Exception as e: print(f"Modal error: {e}, falling back to local") return {"error": f"Local Firdaria not implemented: {e}"} else: return {"error": "Firdaria only available via Modal server"} def calculate_full_dignities( self, planet: str, sign: str, degree: float, is_day_chart: bool ) -> Dict[str, Any]: """Calculate complete essential dignities including bounds and decans""" # Get bound bound_result = self.get_bound_ruler(sign, degree) decan_result = self.get_decan_ruler(sign, degree) # Calculate base dignities (from app.py) from app import (DOMICILE_RULERS, EXALTATIONS, DETRIMENTS, FALLS, TRIPLICITIES, SIGN_TRIPLICITIES) dignity_score = 0 dignities = {} # Domicile if DOMICILE_RULERS.get(sign) == planet: dignity_score += 5 dignities["domicile"] = {"points": 5, "ruler": planet} # Exaltation if EXALTATIONS.get(sign) == planet: dignity_score += 4 dignities["exaltation"] = {"points": 4, "ruler": planet} # Triplicity triplicity = SIGN_TRIPLICITIES.get(sign) if triplicity: trip_rulers = TRIPLICITIES[triplicity] if is_day_chart and trip_rulers["day"] == planet: dignity_score += 3 dignities["triplicity"] = {"points": 3, "type": "day", "ruler": planet} elif not is_day_chart and trip_rulers["night"] == planet: dignity_score += 3 dignities["triplicity"] = {"points": 3, "type": "night", "ruler": planet} elif trip_rulers["participating"] == planet: dignity_score += 3 dignities["triplicity"] = {"points": 3, "type": "participating", "ruler": planet} # Bound if "bound_ruler" in bound_result and bound_result["bound_ruler"] == planet: dignity_score += 2 dignities["bound"] = bound_result # Decan if "decan_ruler" in decan_result and decan_result["decan_ruler"] == planet: dignity_score += 1 dignities["decan"] = decan_result # Detriment if sign in DETRIMENTS.get(planet, []): dignity_score -= 5 dignities["detriment"] = {"points": -5, "sign": sign} # Fall if sign in FALLS.get(planet, []): dignity_score -= 4 dignities["fall"] = {"points": -4, "sign": sign} return { "planet": planet, "sign": sign, "degree": degree, "sect": "day" if is_day_chart else "night", "dignities": dignities, "total_score": dignity_score } def get_tools(self) -> List[Dict[str, Any]]: """Get list of available MCP tools""" tools = [ { "name": "get_bound_ruler", "description": "Get Egyptian bound (term) ruler for a position", "parameters": ["sign", "degree"], "available": "local+modal" }, { "name": "get_decan_ruler", "description": "Get decan (face) ruler for a position", "parameters": ["sign", "degree"], "available": "local+modal" }, { "name": "calculate_full_dignities", "description": "Calculate complete essential dignities", "parameters": ["planet", "sign", "degree", "is_day_chart"], "available": "local+modal" }, { "name": "calculate_zodiacal_releasing", "description": "Calculate Zodiacal Releasing periods", "parameters": ["starting_sign", "birth_date", "calculation_date"], "available": "modal" if self.mode == "modal" else "modal-only" }, { "name": "calculate_firdaria", "description": "Calculate Firdaria (Persian time-lord) periods", "parameters": ["birth_date", "is_day_chart", "years_to_calculate"], "available": "modal" if self.mode == "modal" else "modal-only" } ] return tools # Convenience functions def create_mcp_client(prefer_modal: bool = True) -> MCPClient: """Create an MCP client, preferring Modal if available""" if prefer_modal and MODAL_AVAILABLE: return MCPClient(mode="modal") return MCPClient(mode="local")