#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Jyotish MCP Server v1.0 Exposes Jyotish-Vedic-Astrology calculation engine as MCP tools. Install: pip install mcp Run: python3 mcp_server.py Add to ~/.workbuddy/mcp.json: { "mcpServers": { "jyotish": { "command": "/Users/wuyongnaren/.workbuddy/binaries/python/versions/3.13.12/bin/python3", "args": ["/Users/wuyongnaren/.workbuddy/skills/jyotish-vedic-astrology/mcp_server.py"], "env": {} } } } """ import sys import os import json import subprocess import asyncio from typing import Dict, Any, Optional, List # Add scripts dir to path so imports work SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(SCRIPT_DIR, "scripts")) from mcp.server.fastmcp import FastMCP # ============================================================================ # MCP Server # ============================================================================ mcp = FastMCP( "jyotish-vedic-astrology", instructions=( "Jyotish (Vedic Astrology) calculation engine. " "Provides chart calculation, Vimshottari Dasha, Shadbala, " "Ashtakavarga, Nakshatra analysis, and full-reading synthesis. " "All calculations use Swiss Ephemeris (Lahiri ayanamsa). " "IMPORTANT: partial techniques (marked in audit) are approximate " "and should NOT be used as sole evidence for high-stakes predictions." ), ) # ============================================================================ # Helpers # ============================================================================ def _run_engine(subcommand: str, args: Dict[str, Any]) -> Dict[str, Any]: """Run jyotish_engine.py subcommand and return parsed JSON output.""" engine = os.path.join(SCRIPT_DIR, "scripts", "jyotish_engine.py") cmd = [sys.executable, engine, subcommand] for k, v in args.items(): if v is None: continue flag = "--" + k.replace("_", "-") if isinstance(v, bool): if v: cmd.append(flag) else: cmd.extend([flag, str(v)]) result = subprocess.run( cmd, capture_output=True, text=True, timeout=120, cwd=SCRIPT_DIR ) if result.returncode != 0: return {"error": True, "stderr": result.stderr, "stdout": result.stdout} try: return json.loads(result.stdout) except json.JSONDecodeError: return {"raw_output": result.stdout} def _audit_status() -> Dict[str, Any]: """Run audit and return structured status.""" audit = os.path.join(SCRIPT_DIR, "scripts", "audit_capabilities.py") result = subprocess.run( [sys.executable, audit, "--mode", "validate"], capture_output=True, text=True, timeout=30, cwd=SCRIPT_DIR ) try: return json.loads(result.stdout) except Exception: return {"valid": False, "raw": result.stdout} def _safe_get(data: Dict[str, Any], *path: str) -> Any: cur: Any = data for part in path: if not isinstance(cur, dict) or part not in cur: return None cur = cur[part] return cur def _convergence_score(convergence: Any) -> int: if not isinstance(convergence, dict): return 0 level = convergence.get("convergence_level") mapping = {"L1": 20, "L2": 40, "L3": 60, "L4": 80, "L5": 95} return mapping.get(level, 0) _SIGNS = [ "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces", ] _SIGN_TO_INDEX = {name: idx for idx, name in enumerate(_SIGNS)} _SIGN_LORDS = { "Aries": "Mars", "Taurus": "Venus", "Gemini": "Mercury", "Cancer": "Moon", "Leo": "Sun", "Virgo": "Mercury", "Libra": "Venus", "Scorpio": "Mars", "Sagittarius": "Jupiter", "Capricorn": "Saturn", "Aquarius": "Saturn", "Pisces": "Jupiter", } _NAKSHATRA_NAMES = [ "Ashwini", "Bharani", "Krittika", "Rohini", "Mrigashira", "Ardra", "Punarvasu", "Pushya", "Ashlesha", "Magha", "Purva Phalguni", "Uttara Phalguni", "Hasta", "Chitra", "Swati", "Vishakha", "Anuradha", "Jyeshtha", "Mula", "Purva Ashadha", "Uttara Ashadha", "Shravana", "Dhanishta", "Shatabhisha", "Purva Bhadrapada", "Uttara Bhadrapada", "Revati", ] _NAKSHATRA_LORDS = [ "Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury", "Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury", "Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury", ] _WEALTH_HOUSES = {2, 5, 9, 10, 11} _NAKSHATRA_SPAN = 360.0 / 27.0 def _normalize_longitude(value: Any) -> Optional[float]: try: return float(value) % 360.0 except (TypeError, ValueError): return None def _circular_distance_deg(a: float, b: float) -> float: diff = abs(a - b) % 360.0 return min(diff, 360.0 - diff) def _sign_from_longitude(lon: float) -> str: return _SIGNS[int(lon // 30.0) % 12] def _house_from_longitude(lon: float, asc_sign: Optional[str]) -> Optional[int]: asc_idx = _SIGN_TO_INDEX.get(asc_sign) if asc_sign else None if asc_idx is None: return None return ((int(lon // 30.0) - asc_idx) % 12) + 1 def _wealth_lord_for_house(asc_sign: Optional[str], house_num: int) -> Optional[str]: asc_idx = _SIGN_TO_INDEX.get(asc_sign) if asc_sign else None if asc_idx is None: return None house_sign = _SIGNS[(asc_idx + house_num - 1) % 12] return _SIGN_LORDS.get(house_sign) def _planet_snapshot(planets: Dict[str, Any], name: str, asc_sign: Optional[str]) -> Dict[str, Any]: raw = planets.get(name) if isinstance(planets, dict) else None data = dict(raw) if isinstance(raw, dict) else {} lon = _normalize_longitude(data.get("degree_raw", data.get("degree"))) if lon is not None: data.setdefault("degree_raw", lon) data.setdefault("sign", _sign_from_longitude(lon)) if data.get("house") is None: house = _house_from_longitude(lon, asc_sign) if house is not None: data["house"] = house return data def _derive_yogi_wealth_support(modules: Dict[str, Any]) -> Optional[Dict[str, Any]]: if not isinstance(modules, dict): return None chart = modules.get("chart") if not isinstance(chart, dict): return None ascendant = chart.get("ascendant") if isinstance(chart.get("ascendant"), dict) else {} asc_lon = _normalize_longitude(ascendant.get("degree_raw", ascendant.get("lon", ascendant.get("degree")))) asc_sign = ascendant.get("sign") if asc_sign not in _SIGN_TO_INDEX and asc_lon is not None: asc_sign = _sign_from_longitude(asc_lon) planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {} sun_lon = _normalize_longitude(_safe_get(planets, "Sun", "degree_raw") or _safe_get(planets, "Sun", "degree")) moon_lon = _normalize_longitude(_safe_get(planets, "Moon", "degree_raw") or _safe_get(planets, "Moon", "degree")) if sun_lon is None or moon_lon is None: return None yogi_point_lon = (sun_lon + moon_lon) % 360.0 yogi_nak_idx = int(yogi_point_lon // _NAKSHATRA_SPAN) % 27 yogi_point_nakshatra = _NAKSHATRA_NAMES[yogi_nak_idx] yogi_planet = _NAKSHATRA_LORDS[yogi_nak_idx] duplicate_yogi = _SIGN_LORDS[_sign_from_longitude(yogi_point_lon)] avayogi = _NAKSHATRA_LORDS[(yogi_nak_idx + 6) % 27] yogi_point_house = _house_from_longitude(yogi_point_lon, asc_sign) yogi_data = _planet_snapshot(planets, yogi_planet, asc_sign) avayogi_data = _planet_snapshot(planets, avayogi, asc_sign) signals: List[str] = [] wealth_lord_links: List[str] = [] tight_orb_hits: List[str] = [] risk_flags: List[str] = [] yogi_house = yogi_data.get("house") if yogi_house in _WEALTH_HOUSES: signals.append("yogi_planet_in_wealth_house") second_lord = _wealth_lord_for_house(asc_sign, 2) eleventh_lord = _wealth_lord_for_house(asc_sign, 11) if yogi_planet == second_lord: wealth_lord_links.append("yogi_planet_is_2l") signals.append("yogi_planet_is_2l") if yogi_planet == eleventh_lord: wealth_lord_links.append("yogi_planet_is_11l") signals.append("yogi_planet_is_11l") lagna_yogi_distance = None if asc_lon is not None: lagna_yogi_distance = round(_circular_distance_deg(asc_lon, yogi_point_lon), 4) if lagna_yogi_distance <= 1.0: tight_orb_hits.append("lagna_yogi_tight_orb") signals.append("lagna_yogi_tight_orb") avayogi_house = avayogi_data.get("house") if avayogi_house in _WEALTH_HOUSES: risk_flags.append("avayogi_in_wealth_house") if len(signals) >= 3 and not risk_flags: level = "strong" elif len(signals) >= 2: level = "moderate" else: level = "weak" return { "level": level, "source": "yogi_asc_tight_orb_wealth", "yogi_planet": yogi_planet, "duplicate_yogi": duplicate_yogi, "avayogi": avayogi, "yogi_point_longitude": round(yogi_point_lon, 4), "yogi_point_nakshatra": yogi_point_nakshatra, "yogi_point_house": yogi_point_house, "lagna_yogi_distance_deg": lagna_yogi_distance, "tight_orb_hits": tight_orb_hits, "wealth_lord_links": wealth_lord_links, "signals": signals, "risk_flags": risk_flags, } def _derive_wealth_promise_strength(modules: Dict[str, Any]) -> Optional[Dict[str, Any]]: yogi_support = _derive_yogi_wealth_support(modules) yogas_doshas = modules.get("yogas_doshas") if isinstance(modules, dict) else {} dhana = yogas_doshas.get("dhana_yogas") if isinstance(yogas_doshas, dict) else {} yogas = dhana.get("yogas") if isinstance(dhana, dict) else None has_dhana = False has_lakshmi = False dhana_level = "weak" lakshmi_level = "weak" sources = set() if isinstance(yogas, list) and yogas: for row in yogas: if not isinstance(row, dict): continue lvl = str(row.get("strength", "")).lower() row_type = str(row.get("type", "")).lower() if "lakshmi" in row_type: sources.add("lakshmi") has_lakshmi = True if lvl == "strong" or (lvl == "moderate" and lakshmi_level == "weak"): lakshmi_level = lvl elif "dhana" in row_type: sources.add("dhana") has_dhana = True if lvl == "strong" or (lvl == "moderate" and dhana_level == "weak"): dhana_level = lvl if not has_dhana and not has_lakshmi: return None yogi_level = yogi_support.get("level") if isinstance(yogi_support, dict) else None if yogi_level in {"moderate", "strong"}: sources.add("yogi") supporting_sources = sorted(sources) if has_dhana and has_lakshmi and "yogi" in sources: primary_source = "dhana_lakshmi_yogi_hooks" elif has_dhana and "yogi" in sources: primary_source = "dhana_yogi_hooks" elif has_dhana and has_lakshmi: primary_source = "dhana_lakshmi_hooks" elif has_dhana: primary_source = "dhana_yogas" else: primary_source = "lakshmi_hooks" if dhana_level == "strong" or lakshmi_level == "strong": final_level = "strong" elif dhana_level == "moderate" or lakshmi_level == "moderate": final_level = "moderate" else: final_level = "weak" return { "level": final_level, "primary_source": primary_source, "supporting_sources": supporting_sources, "count": len(yogas) if isinstance(yogas, list) else 0, "source_diversity": len(supporting_sources), "yogi_support": yogi_support if yogi_level in {"moderate", "strong"} else None, } def _check_external_avayogi_risk(result: Dict[str, Any]) -> Optional[Dict[str, Any]]: external_truth = result.get("external_truth") if isinstance(result, dict) else {} avayogi_planet = external_truth.get("avayogi_planet") if isinstance(external_truth, dict) else None if not avayogi_planet: return None modules = result.get("modules", {}) if isinstance(result, dict) else {} chart = modules.get("chart") if isinstance(modules, dict) else {} planets = chart.get("planets") if isinstance(chart, dict) else {} planet_data = planets.get(avayogi_planet) if isinstance(planets, dict) else None if not isinstance(planet_data, dict): return None house = planet_data.get("house") status = str(planet_data.get("status", "")) if "Own Sign" in status or "Moolatrikona" in status or "Exalted" in status: return None signals: List[str] = [] if house in {1, 2, 5, 9, 10, 11}: signals.append("avayogi_in_wealth_house") if not signals: return None return { "planet": avayogi_planet, "house": house, "status": status, "source": "external_avayogi_planet", "risk_level": "moderate", "signals": signals, } def _derive_jaimini_marriage_support(present: Dict[str, Any]) -> Dict[str, Any]: signals: List[str] = [] darakaraka = present.get("darakaraka") upapada_lagna = present.get("upapada_lagna") if darakaraka: signals.append("darakaraka_active") if isinstance(darakaraka, dict): if darakaraka.get("house") == 7 or darakaraka.get("house_from_lagna") == 7: signals.append("dk_7h_link") if darakaraka.get("ul_link") or darakaraka.get("linked_to_ul"): signals.append("dk_ul_link") if isinstance(upapada_lagna, dict) and isinstance(darakaraka, dict): dk_sign = darakaraka.get("sign") ul_sign = upapada_lagna.get("sign") if dk_sign and ul_sign and dk_sign == ul_sign and "dk_ul_link" not in signals: signals.append("dk_ul_link") if present.get("jaimini_timing_support"): signals.append("jaimini_dasha_support") if not signals: level = "none" elif len(signals) == 1: level = "weak" elif "darakaraka_active" in signals: level = "moderate" else: level = "weak" return { "level": level, "signals": signals, "source": "jaimini_bridge_v1", } def _derive_external_activation_support(modules: Dict[str, Any], domain: str) -> Dict[str, Any]: ledger = _safe_get(modules, "external_activation", "evidence_ledger") if not isinstance(ledger, list): return { "level": "none", "source": None, "signals": [], "events": [], } events: List[Dict[str, Any]] = [] for event in ledger: if not isinstance(event, dict): continue if event.get("operation") != "range_scan": continue if event.get("domain") != domain: continue if event.get("source") != "vedastro_service_adapter_candidate": continue events.append(event) if not events: level = "none" elif any((event.get("score") or 0) >= 70 for event in events): level = "moderate" else: level = "weak" return { "level": level, "source": "vedastro_service_adapter_candidate" if events else None, "signals": ["vedastro_range_scan"] if events else [], "events": events, } def _derive_argala_support(modules: Dict[str, Any], target_house: int) -> Dict[str, Any]: house_data = _safe_get(modules, "argala", "houses", f"house_{target_house}") if not isinstance(house_data, dict): return { "level": "none", "target_house": target_house, "source": "argala_house_bridge_v1", "signals": [], "raw": None, } net_result = house_data.get("net_result") if net_result == "supported": level = "supportive" signals = ["argala_support"] elif net_result == "obstructed": level = "obstructive" signals = ["virodhargala_obstruction"] else: level = "none" signals = [] return { "level": level, "target_house": target_house, "source": "argala_house_bridge_v1", "signals": signals, "raw": { "net_result": net_result, "argala_count": house_data.get("argala_count"), "virodhargala_count": house_data.get("virodhargala_count"), }, } def _sign_to_index(sign: str) -> Optional[int]: try: return _SIGNS.index(sign) except ValueError: return None def _lord_for_house_from_lagna(asc_sign: Optional[str], house_num: int) -> Optional[str]: asc_idx = _sign_to_index(asc_sign) if asc_sign else None if asc_idx is None: return None sign = _SIGNS[(asc_idx + house_num - 1) % 12] return _SIGN_LORDS.get(sign) def _extract_dignity_code(status: str) -> Optional[str]: if "Neecha Bhanga" in status or "落陷取消" in status: return "NEECHA_BHANGA" if "Great Enemy" in status or "极敌" in status: return "GREAT_ENEMY" return None def _derive_dignity_guardrail(route: str, present: Dict[str, Any]) -> Dict[str, Any]: base = { "route": route, "status": "blocked", "score_delta": 0, "source": "chart.planets.status", "relevant_planets": [], "ignored_planets": [], "conflict_flags": [], "notes": ["Only domain-relevant planets are allowed to affect score."], } chart = present.get("chart") if isinstance(present.get("chart"), dict) else {} ascendant = chart.get("ascendant") if isinstance(chart.get("ascendant"), dict) else {} planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {} asc_sign = ascendant.get("sign") if not asc_sign or not isinstance(planets, dict) or not planets: return base relevant_roles: Dict[str, str] = {} if route == "relationship": lord_7 = _lord_for_house_from_lagna(asc_sign, 7) if not lord_7: return base relevant_roles[lord_7] = "7l" relevant_roles["Venus"] = "relationship_karaka" relevant_roles["Jupiter"] = "relationship_support" darakaraka = present.get("darakaraka") if isinstance(darakaraka, dict) and darakaraka.get("planet"): relevant_roles[darakaraka["planet"]] = "darakaraka" elif route == "finance": lord_2 = _lord_for_house_from_lagna(asc_sign, 2) lord_11 = _lord_for_house_from_lagna(asc_sign, 11) if not lord_2 or not lord_11: return base relevant_roles[lord_2] = "2l" relevant_roles[lord_11] = "11l" relevant_roles["Venus"] = "finance_karaka" relevant_roles["Jupiter"] = "finance_support" if present.get("career_convergence"): lord_10 = _lord_for_house_from_lagna(asc_sign, 10) if lord_10: relevant_roles[lord_10] = "10l_career_monetization" else: base["status"] = "ok" return base supportive_hits: List[str] = [] friction_hits: List[str] = [] for planet_name, pdata in planets.items(): if planet_name not in relevant_roles: base["ignored_planets"].append({ "planet": planet_name, "reason": "not_domain_relevant", }) continue if not isinstance(pdata, dict): return base status = str(pdata.get("status", "")) if not status: return base dignity_code = _extract_dignity_code(status) effect = ( "supportive_recovery" if dignity_code == "NEECHA_BHANGA" else "high_friction" if dignity_code == "GREAT_ENEMY" else "none" ) base["relevant_planets"].append({ "planet": planet_name, "role": relevant_roles[planet_name], "status": status, "dignity_code": dignity_code, "effect": effect, }) if dignity_code == "NEECHA_BHANGA": supportive_hits.append(planet_name) elif dignity_code == "GREAT_ENEMY": friction_hits.append(planet_name) if supportive_hits and friction_hits: base["status"] = "conflict" base["conflict_flags"] = [ "neecha_bhanga_on_key_significator", "great_enemy_on_key_significator", ] base["score_delta"] = 0 elif supportive_hits: base["status"] = "caution" base["score_delta"] = 5 elif friction_hits: base["status"] = "caution" base["score_delta"] = -5 else: base["status"] = "ok" base["score_delta"] = 0 return base def _derive_event_judgement(route: str, present: Dict[str, Any], missing: List[str]) -> Dict[str, Any]: if route == "career": score = 0 score += 20 if present.get("d10_dasamsa") else 0 score += 15 if present.get("a10_karma_pada") else 0 score += 15 if present.get("amatyakaraka") else 0 score += 10 if present.get("karakamsha") else 0 score += 10 if present.get("vimshottari_current") else 0 score += 10 if present.get("narayana_current") else 0 score += _convergence_score(present.get("career_convergence")) argala_support = present.get("argala_support") or {} if argala_support.get("level") == "supportive": score += 5 elif argala_support.get("level") == "obstructive": score -= 5 external_activation = present.get("external_activation") or {} if external_activation.get("level") == "moderate": score += 5 if missing: score = min(score, 35) score = min(score, 100) if missing: verdict = "insufficient_evidence" elif score >= 80: verdict = "high_probability_window" elif score >= 60: verdict = "moderate_probability_window" elif score >= 40: verdict = "weak_window_needs_confirmation" else: verdict = "insufficient_evidence" secondary_context: List[str] = [] if present.get("a10_karma_pada"): secondary_context.append("a10_active") if present.get("amatyakaraka"): secondary_context.append("amk_active") if present.get("karakamsha"): secondary_context.append("karakamsha_context") if argala_support.get("level") == "supportive": secondary_context.append("argala_support") elif argala_support.get("level") == "obstructive": secondary_context.append("virodhargala_obstruction") if external_activation.get("level") == "moderate": secondary_context.append("external_activation_support") hard_gate_missing = any( key in missing for key in ( "d10_dasamsa", "a10_karma_pada", "vimshottari_current", "narayana_current", ) ) dominant_label = None if not hard_gate_missing and present.get("career_convergence") and score >= 60: dominant_label = "career_status" return { "event_family": "career", "score": score, "verdict": verdict, "dominant_label": dominant_label, "secondary_context": secondary_context, "primary_drivers": [ key for key in ( "career_convergence", "vimshottari_current", "narayana_current", "a10_karma_pada", "amatyakaraka", "karakamsha", "argala_support", ) if present.get(key) ], } if route == "relationship": score = 0 score += 15 if present.get("d9_navamsa") else 0 score += 15 if present.get("upapada_lagna") else 0 score += 15 if present.get("darakaraka") else 0 score += 10 if present.get("vivah_saham") else 0 score += 10 if present.get("vimshottari_current") else 0 score += 10 if present.get("narayana_current") else 0 score += _convergence_score(present.get("marriage_convergence")) dignity_guardrail = present.get("dignity_guardrail") or {} score += dignity_guardrail.get("score_delta", 0) argala_support = present.get("argala_support") or {} if argala_support.get("level") == "supportive": score += 5 elif argala_support.get("level") == "obstructive": score -= 5 if missing: score = min(score, 35) score = min(score, 100) if missing: verdict = "insufficient_evidence" elif score >= 80: verdict = "high_probability_window" elif score >= 60: verdict = "moderate_probability_window" elif score >= 40: verdict = "weak_window_needs_confirmation" else: verdict = "insufficient_evidence" jaimini_support = present.get("jaimini_marriage_support") or {} secondary_context: List[str] = [] if present.get("darakaraka"): secondary_context.append("darakaraka_active") if jaimini_support.get("level") == "moderate": secondary_context.append("jaimini_support") if present.get("upapada_lagna"): secondary_context.append("ul_support") if argala_support.get("level") == "supportive": secondary_context.append("argala_support") elif argala_support.get("level") == "obstructive": secondary_context.append("virodhargala_obstruction") external_activation = present.get("external_activation") or {} if external_activation.get("level") == "moderate": secondary_context.append("external_activation_support") if dignity_guardrail.get("status") == "conflict": secondary_context.append("dignity_conflict") elif dignity_guardrail.get("score_delta") == 5: secondary_context.append("dignity_supportive_recovery") elif dignity_guardrail.get("score_delta") == -5: secondary_context.append("dignity_high_friction") hard_gate_missing = any( key in missing for key in ( "d9_navamsa", "upapada_lagna", "vimshottari_current", "narayana_current", ) ) label_support_present = bool(present.get("vivah_saham") or present.get("marriage_convergence")) dominant_label = None if ( not hard_gate_missing and label_support_present and jaimini_support.get("level") == "moderate" ): dominant_label = "legal_marriage" return { "event_family": "relationship", "score": score, "verdict": verdict, "dominant_label": dominant_label, "secondary_context": secondary_context, "primary_drivers": [ key for key in ( "marriage_convergence", "vimshottari_current", "narayana_current", "darakaraka", "upapada_lagna", "argala_support", ) if present.get(key) ], } if route == "finance": score = 0 wealth_promise = present.get("wealth_promise_strength") wealth_promise_level = wealth_promise.get("level") if isinstance(wealth_promise, dict) else None wealth_promise_diversity = wealth_promise.get("source_diversity", 0) if isinstance(wealth_promise, dict) else 0 avayogi_risk = present.get("avayogi_risk") score += 15 if present.get("d2_hora") else 0 score += 10 if present.get("d10_dasamsa") else 0 score += 10 if present.get("shadbala") else 0 score += 10 if present.get("ashtakavarga_house_scores") else 0 score += 10 if present.get("vimshottari_current") else 0 score += 10 if present.get("narayana_current") else 0 score += 20 if wealth_promise_level == "strong" else 10 if wealth_promise_level == "moderate" else 0 score += 5 if wealth_promise_diversity >= 2 else 0 score += max( _convergence_score(present.get("wealth_convergence")), _convergence_score(present.get("gains_convergence")), _convergence_score(present.get("career_convergence")), ) dignity_guardrail = present.get("dignity_guardrail") or {} score += dignity_guardrail.get("score_delta", 0) score -= 5 if isinstance(avayogi_risk, dict) and avayogi_risk.get("risk_level") == "moderate" else 0 public_wealth_lift = ( not missing and bool(present.get("wealth_convergence")) and ( bool(present.get("gains_convergence")) or bool(present.get("career_convergence")) ) and bool(present.get("vimshottari_current")) and bool(present.get("narayana_current")) ) if missing: score = min(score, 35) score = min(score, 100) if missing: verdict = "insufficient_evidence" elif public_wealth_lift and score >= 60: verdict = "moderate_probability_window" elif score >= 80: verdict = "high_probability_window" elif score >= 60: verdict = "moderate_probability_window" elif score >= 40: verdict = "weak_window_needs_confirmation" else: verdict = "insufficient_evidence" payout_label = None dominant_label = None secondary_context: List[str] = [] gains_score = _convergence_score(present.get("gains_convergence")) wealth_score = _convergence_score(present.get("wealth_convergence")) career_score = _convergence_score(present.get("career_convergence")) if gains_score >= 60 and wealth_score < 40 and career_score < 40: payout_label = "income_growth" dominant_label = "income_growth" secondary_context = ["wealth_family"] if present.get("wealth_convergence") else [] elif public_wealth_lift and score >= 60: payout_label = "public_wealth_status" dominant_label = "public_wealth_status" if present.get("career_convergence"): secondary_context.append("career_status") if present.get("gains_convergence"): secondary_context.append("gains_wishes") if isinstance(avayogi_risk, dict) and avayogi_risk.get("risk_level") == "moderate": secondary_context.append("avayogi_active") external_activation = present.get("external_activation") or {} if external_activation.get("level") == "moderate": secondary_context.append("external_activation_support") if dignity_guardrail.get("status") == "conflict": secondary_context.append("dignity_conflict") elif dignity_guardrail.get("score_delta") == 5: secondary_context.append("dignity_supportive_recovery") elif dignity_guardrail.get("score_delta") == -5: secondary_context.append("dignity_high_friction") return { "event_family": "finance", "score": score, "verdict": verdict, "payout_label": payout_label, "dominant_label": dominant_label, "secondary_context": secondary_context, "primary_drivers": [ key for key in ( "wealth_convergence", "gains_convergence", "career_convergence", "vimshottari_current", "narayana_current", ) if present.get(key) ], } return { "event_family": route, "score": 0, "verdict": "context_only", "primary_drivers": [], } def _collect_strict_evidence(route: str, result: Dict[str, Any]) -> Dict[str, Any]: modules = result.get("modules", {}) if isinstance(result, dict) else {} domain_activations = _safe_get(modules, "dasa_convergence", "domain_activations") or {} if route == "career": required = [ "varga_full.D10_Dasamsa", "special_lagnas.A10_Karma_Pada", "jaimini.karakas.Amatyakaraka", "jaimini.karakamsha", "dasha.current_dasha", "narayana_dasha.current_dasha", "dasa_convergence.domain_activations.career_status", ] present = { "d10_dasamsa": _safe_get(modules, "varga_full", "D10_Dasamsa"), "a10_karma_pada": _safe_get(modules, "special_lagnas", "A10_Karma_Pada"), "amatyakaraka": _safe_get(modules, "jaimini", "karakas", "Amatyakaraka"), "karakamsha": _safe_get(modules, "jaimini", "karakamsha"), "vimshottari_current": _safe_get(modules, "dasha", "current_dasha"), "narayana_current": _safe_get(modules, "narayana_dasha", "current_dasha"), "career_convergence": domain_activations.get("career_status"), } present["argala_support"] = _derive_argala_support(modules, 10) present["external_activation"] = _derive_external_activation_support(modules, "career") missing = [key for key, value in present.items() if key not in { "external_activation", "argala_support" } and value in (None, {}, [], "")] convergence = present["career_convergence"] or {} confidence_cap = "medium" if missing: confidence_cap = "low" elif convergence.get("convergence_level") in {"L4", "L5"}: confidence_cap = "medium-high" elif convergence.get("convergence_level") == "L3": confidence_cap = "medium" else: confidence_cap = "medium-low" event_judgement = _derive_event_judgement(route, present, missing) return { "question_type": route, "required_evidence": required, "present_evidence": present, "missing_evidence": missing, "confidence_cap": confidence_cap, "blocked": bool(missing), "event_judgement": event_judgement, "reason": ( "Career timing requires D10 + A10/Karma Pada + AmK/Karakamsha " "plus dual dasha and career convergence support." ), } if route == "relationship": required = [ "varga_full.D9_Navamsa", "special_lagnas.Upapada_Lagna", "jaimini.darakaraka", "vivah_saham", "dasha.current_dasha", "narayana_dasha.current_dasha", "dasa_convergence.domain_activations.marriage_partnership", ] present = { "d9_navamsa": _safe_get(modules, "varga_full", "D9_Navamsa"), "upapada_lagna": _safe_get(modules, "special_lagnas", "Upapada_Lagna"), "darakaraka": _safe_get(modules, "jaimini", "darakaraka"), "vivah_saham": _safe_get(modules, "vivah_saham"), "chart": _safe_get(modules, "chart"), "vimshottari_current": _safe_get(modules, "dasha", "current_dasha"), "narayana_current": _safe_get(modules, "narayana_dasha", "current_dasha"), "marriage_convergence": domain_activations.get("marriage_partnership"), } present["jaimini_timing_support"] = _safe_get(modules, "jaimini", "marriage_timing_support") present["jaimini_marriage_support"] = _derive_jaimini_marriage_support(present) present["argala_support"] = _derive_argala_support(modules, 7) present["external_activation"] = _derive_external_activation_support(modules, "marriage") present["dignity_guardrail"] = _derive_dignity_guardrail(route, present) missing = [ key for key, value in present.items() if key not in {"chart", "external_activation", "dignity_guardrail", "jaimini_marriage_support", "jaimini_timing_support", "argala_support"} and value in (None, {}, [], "") ] convergence = present["marriage_convergence"] or {} confidence_cap = "medium" if missing: confidence_cap = "low" elif present["dignity_guardrail"].get("status") == "conflict": confidence_cap = "low" elif convergence.get("convergence_level") in {"L4", "L5"}: confidence_cap = "medium-high" elif convergence.get("convergence_level") == "L3": confidence_cap = "medium" else: confidence_cap = "medium-low" event_judgement = _derive_event_judgement(route, present, missing) return { "question_type": route, "required_evidence": required, "present_evidence": present, "missing_evidence": missing, "confidence_cap": confidence_cap, "blocked": bool(missing), "event_judgement": event_judgement, "reason": ( "Marriage timing requires D9 + UL + DK + dual dasha + Vivah Saham " "and convergence support; missing links cap confidence." ), } if route == "finance": avayogi_risk = _check_external_avayogi_risk(result) required = [ "varga_full.D2_Hora", "varga_full.D10_Dasamsa", "shadbala.planets", "ashtakavarga.house_scores", "dasha.current_dasha", "narayana_dasha.current_dasha", "dasa_convergence.domain_activations.wealth_family", ] present = { "d2_hora": _safe_get(modules, "varga_full", "D2_Hora"), "d10_dasamsa": _safe_get(modules, "varga_full", "D10_Dasamsa"), "shadbala": _safe_get(modules, "shadbala", "planets"), "ashtakavarga_house_scores": _safe_get(modules, "ashtakavarga", "house_scores"), "vimshottari_current": _safe_get(modules, "dasha", "current_dasha"), "narayana_current": _safe_get(modules, "narayana_dasha", "current_dasha"), "wealth_convergence": domain_activations.get("wealth_family"), "gains_convergence": domain_activations.get("gains_wishes"), "chart": _safe_get(modules, "chart"), "career_convergence": domain_activations.get("career_status"), "wealth_promise_strength": _derive_wealth_promise_strength(modules), "avayogi_risk": avayogi_risk, } present["external_activation"] = _derive_external_activation_support(modules, "wealth") present["dignity_guardrail"] = _derive_dignity_guardrail(route, present) missing = [key for key, value in present.items() if key not in { "chart", "external_activation", "dignity_guardrail", "gains_convergence", "career_convergence", "avayogi_risk" } and value in (None, {}, [], "")] convergence_hits: List[Dict[str, Any]] = [ item for item in [ present["wealth_convergence"], present["gains_convergence"], present["career_convergence"], ] if isinstance(item, dict) and item ] confidence_cap = "medium" if missing: confidence_cap = "low" elif present["dignity_guardrail"].get("status") == "conflict": confidence_cap = "low" elif any(hit.get("convergence_level") in {"L4", "L5"} for hit in convergence_hits): confidence_cap = "medium-high" elif convergence_hits: confidence_cap = "medium" else: confidence_cap = "medium-low" event_judgement = _derive_event_judgement(route, present, missing) promise = present.get("wealth_promise_strength") or {} if "yogi" in promise.get("supporting_sources", []) and event_judgement.get("dominant_label") and "yogi_active" not in event_judgement.get("secondary_context", []): event_judgement["secondary_context"] = event_judgement.get("secondary_context", []) + ["yogi_active"] return { "question_type": route, "required_evidence": required, "present_evidence": present, "missing_evidence": missing, "confidence_cap": confidence_cap, "blocked": bool(missing), "event_judgement": event_judgement, "reason": ( "Finance timing requires D2/D10 + strength + SAV + dual dasha " "plus at least one wealth-related convergence domain." ), } return { "question_type": route, "required_evidence": [], "present_evidence": {}, "missing_evidence": [], "confidence_cap": "context-only", "blocked": False, "event_judgement": _derive_event_judgement(route, {}, []), "reason": "Route-specific strict evidence audit is currently implemented for relationship and finance timing.", } # ============================================================================ # Tools # ============================================================================ @mcp.tool() def calculate_chart( year: int, month: int, day: int, hour: int, minute: int, lat: float, lon: float, tz: float, node_mode: str = "mean", ) -> Dict[str, Any]: """ Calculate a complete Vedic birth chart (D1 Rashi). Returns: planets with sidereal longitudes, houses (whole-sign), Nakshatra placements, dignity levels, and combustion status. Uses Swiss Ephemeris with Lahiri ayanamsa. Args: year: Birth year (e.g. 1990) month: Birth month (1-12) day: Birth day (1-31) hour: Birth hour (0-23) minute: Birth minute (0-59) lat: Latitude in decimal degrees (north positive, e.g. 28.61) lon: Longitude in decimal degrees (east positive, e.g. 77.20) tz: Timezone offset from UTC in hours (e.g. 5.5 for IST, 8.0 for CST) node_mode: 'mean' (default) or 'true' for lunar node calculation Returns: JSON with planets, houses, ascendant, Nakshatras, dignities """ return _run_engine("chart", { "year": year, "month": month, "day": day, "hour": hour, "minute": minute, "lat": lat, "lon": lon, "tz": tz, "node_mode": node_mode, }) @mcp.tool() def calculate_dasha( year: int, month: int, day: int, hour: int, minute: int, lat: float, lon: float, tz: float, start_date: Optional[str] = None, years: int = 10, node_mode: str = "mean", ) -> Dict[str, Any]: """ Calculate Vimshottari Dasha (planetary period) timeline. Returns the hierarchical Dasha timeline (Maha Dasha → Antar Dasha → Pratyantar) from birth or from a specified start_date. Args: year, month, day, hour, minute: Birth datetime lat, lon: Birth place coordinates tz: Timezone offset from UTC start_date: Optional start date (YYYY-MM-DD) for Dasha from a specific date years: Number of years to calculate from birth (default 10) node_mode: 'mean' or 'true' Returns: JSON with Dasha periods, start/end dates, and current Dasha at birth """ args = { "year": year, "month": month, "day": day, "hour": hour, "minute": minute, "lat": lat, "lon": lon, "tz": tz, "years": years, "node_mode": node_mode, } if start_date: args["start_date"] = start_date return _run_engine("dasha", args) @mcp.tool() def calculate_shadbala( year: int, month: int, day: int, hour: int, minute: int, lat: float, lon: float, tz: float, node_mode: str = "mean", ) -> Dict[str, Any]: """ Calculate Shadbala (six-fold planetary strength). Returns the six components of planetary strength: Sthana Bala (positional), Dig Bala (directional), Kala Bala (temporal), Chesta Bala (motional), Naisargika Bala (natural), Drik Bala (aspectual). NOTE: This is currently a PARTIAL implementation (v6.0.11). Internal invariants pass (1200/1200) but external absolute calibration against JHora/PyJHora/BV Raman is NOT yet complete. Use for relative strength comparison only, NOT for absolute assertions. Args: year, month, day, hour, minute: Birth datetime lat, lon: Birth place coordinates tz: Timezone offset from UTC node_mode: 'mean' or 'true' Returns: JSON with Shadbala components and total scores per planet """ return _run_engine("shadbala", { "year": year, "month": month, "day": day, "hour": hour, "minute": minute, "lat": lat, "lon": lon, "tz": tz, "node_mode": node_mode, }) @mcp.tool() def calculate_ashtakavarga( year: int, month: int, day: int, hour: int, minute: int, lat: float, lon: float, tz: float, node_mode: str = "mean", ) -> Dict[str, Any]: """ Calculate Ashtakavarga (eight-fold strength matrix). Returns the Ashtakavarga table (bindus contributed by each planet to each house) and the Sarva Ashtakavarga (SAV) total for each house. Uses BPHS complete table (SAV=337 total). Args: year, month, day, hour, minute: Birth datetime lat, lon: Birth place coordinates tz: Timezone offset from UTC node_mode: 'mean' or 'true' Returns: JSON with per-planet Ashtakavarga tables and SAV totals """ return _run_engine("ashtakavarga", { "year": year, "month": month, "day": day, "hour": hour, "minute": minute, "lat": lat, "lon": lon, "tz": tz, "node_mode": node_mode, }) @mcp.tool() def calculate_varga( year: int, month: int, day: int, hour: int, minute: int, lat: float, lon: float, tz: float, varga: str = "D9", node_mode: str = "mean", ) -> Dict[str, Any]: """ Calculate a specific Varga (divisional chart). Supported vargas: D9 (Navamsa), D10 (Dasamsa), D12 (Dwadasamsa), D16 (Shodasamsa), D20 (Vimsamsa), D24 (Chaturvimsamsa), D30 (Trimshamsa), D40 (Khavedamsa), D45 (Akshavedamsa), D60 (Shastiamsa). Args: year, month, day, hour, minute: Birth datetime lat, lon: Birth place coordinates tz: Timezone offset from UTC varga: Varga code (default 'D9' for Navamsa) node_mode: 'mean' or 'true' Returns: JSON with varga chart planets and house placements """ return _run_engine("varga", { "year": year, "month": month, "day": day, "hour": hour, "minute": minute, "lat": lat, "lon": lon, "tz": tz, "varga": varga, "node_mode": node_mode, }) @mcp.tool() def calculate_varga_full( year: int, month: int, day: int, hour: int, minute: int, lat: float, lon: float, tz: float, node_mode: str = "mean", ) -> Dict[str, Any]: """ Calculate ALL Vargas (D2 through D60) in one call. Returns the complete BPHS sixteen-varga system. D2=Hora, D3=Drekkana, D4=Chaturthamsa, D7=Saptamsa, D9=Navamsa, D10=Dasamsa, D12=Dwadasamsa, D16=Shodasamsa, D20=Vimsamsa, D24=Chaturvimsamsa, D30=Trimshamsa, D40=Khavedamsa, D45=Akshavedamsa, D60=Shastiamsa. Args: year, month, day, hour, minute: Birth datetime lat, lon: Birth place coordinates tz: Timezone offset from UTC node_mode: 'mean' or 'true' Returns: JSON with all varga charts """ return _run_engine("varga-full", { "year": year, "month": month, "day": day, "hour": hour, "minute": minute, "lat": lat, "lon": lon, "tz": tz, "node_mode": node_mode, }) @mcp.tool() def analyze_nakshatra( year: int, month: int, day: int, hour: int, minute: int, lat: float, lon: float, tz: float, mode: str = "full", node_mode: str = "mean", ) -> Dict[str, Any]: """ Advanced Nakshatra analysis (Chandra Bala, Tara Bala, combined score). Modes: - 'chandra': Chandra Bala only (Moon's strength in Nakshatras) - 'tara': Tara Bala only (constellation-based fortune timing) - 'combined': Both Chandra + Tara with combined score - 'full': Full Nakshatra report with Dasha overlay Args: year, month, day, hour, minute: Birth datetime lat, lon: Birth place coordinates tz: Timezone offset from UTC mode: 'chandra' | 'tara' | 'combined' | 'full' (default 'full') node_mode: 'mean' or 'true' Returns: JSON with Nakshatra analysis results """ return _run_engine("nakshatra-adv", { "year": year, "month": month, "day": day, "hour": hour, "minute": minute, "lat": lat, "lon": lon, "tz": tz, "mode": mode, "node_mode": node_mode, }) @mcp.tool() def calculate_yogas( year: int, month: int, day: int, hour: int, minute: int, lat: float, lon: float, tz: float, node_mode: str = "mean", ) -> Dict[str, Any]: """ Detect Yogas (planetary combinations) in the birth chart. Detects: - Raja Yogas (power/combin status) - Dhana Yogas (wealth combinations) - Pancha Mahapurusha Yogas (great person combinations) - Neecha Bhanga Raja Yoga (cancellation of debility) - Many more from classical texts NOTE: Partial implementation. Not all 284 yoga variants from PyJHora are covered. Use as辅助参考, not sole evidence. Args: year, month, day, hour, minute: Birth datetime lat, lon: Birth place coordinates tz: Timezone offset from UTC node_mode: 'mean' or 'true' Returns: JSON with detected yogas and their strengths """ return _run_engine("yoga", { "year": year, "month": month, "day": day, "hour": hour, "minute": minute, "lat": lat, "lon": lon, "tz": tz, "node_mode": node_mode, }) @mcp.tool() def calculate_transit( year: int, month: int, day: int, hour: int, minute: int, lat: float, lon: float, tz: float, transit_date: str, node_mode: str = "mean", ) -> Dict[str, Any]: """ Calculate planetary transits for a specific date. Returns true sidereal positions of all planets for the transit date, plus double-transit analysis (Saturn + Jupiter) for event timing. Args: year, month, day, hour, minute: Birth datetime (for natal reference) lat, lon: Birth place coordinates tz: Timezone offset from UTC transit_date: Transit date to analyze (YYYY-MM-DD format) node_mode: 'mean' or 'true' Returns: JSON with transit positions and double-transit analysis """ return _run_engine("transit", { "year": year, "month": month, "day": day, "hour": hour, "minute": minute, "lat": lat, "lon": lon, "tz": tz, "transit_date": transit_date, "node_mode": node_mode, }) @mcp.tool() def full_reading( year: int, month: int, day: int, hour: int, minute: int, lat: float, lon: float, tz: float, age: int, transit_date: str, node_mode: str = "mean", ) -> Dict[str, Any]: """ Full Jyotish reading: all techniques in one synthesized report. This is the flagship command. It runs the complete analysis pipeline: D1 chart → D9 Navamsa → D10 Dasamsa → Vimshottari Dasha → Dasha Sandhi → Narayana Dasha → Solar Return → Nakshatra Advanced → Shadbala → Ashtakavarga → Transit → Argala → A10 Karma Pada → UL Upapada → Vargottama → Pushkara → Yogas/Doshas → and more. The output includes a Technique Audit Table showing which techniques are covered (verified) vs partial (approximate). Args: year, month, day, hour, minute: Birth datetime lat, lon: Birth place coordinates tz: Timezone offset from UTC age: Current age of the person (used for age-appropriate analysis) transit_date: Transit date for prediction (YYYY-MM-DD) node_mode: 'mean' or 'true' Returns: JSON with complete reading: all modules, synthesis, audit table """ return _run_engine("full-reading", { "year": year, "month": month, "day": day, "hour": hour, "minute": minute, "lat": lat, "lon": lon, "tz": tz, "age": age, "transit_date": transit_date, "node_mode": node_mode, }) @mcp.tool() def get_audit_status() -> Dict[str, Any]: """ Get the technique registry audit status. Returns which of the 44 techniques are covered (verified against authoritative sources), partial (implemented but not fully benchmarked), or missing. Also returns any warnings or problems. Use this before making predictions to know which techniques are reliable. Returns: JSON with technique_count, covered/partial/missing counts, warnings, and the full technique registry """ return _audit_status() @mcp.tool() def strict_workflow( question: str, year: int, month: int, day: int, hour: int, minute: int, lat: float, lon: float, tz: float, age: int, transit_date: str, node_mode: str = "mean", ) -> Dict[str, Any]: """ Strict workflow router: routes question to the correct analysis path. Instead of running all techniques, this selects the optimal technique combination based on the question type: - Career questions → D10 + Dasha + Shadbala + Transit - Relationship questions → D9 + UL + Dasha + Nakshatra - Financial questions → D2 + D11 + Dasha + Shadbala - Event timing → Dasha + Transit + Gochara This produces higher-confidence results than full-reading for specific questions. Args: question: The user's question in natural language (e.g. 'When will I get married?', 'Career change?') year, month, day, hour, minute: Birth datetime lat, lon: Birth place coordinates tz: Timezone offset from UTC age: Current age transit_date: Transit date for prediction (YYYY-MM-DD) node_mode: 'mean' or 'true' Returns: JSON with routed analysis and confidence level """ q = question.lower() if any(k in q for k in ("career", "job", "work", "promotion", "business", "profession", "事业", "工作", "升职", "生意")): route = "career" focus_techniques = ["D10", "Dasha", "Shadbala", "Transit", "Narayana Dasha"] elif any(k in q for k in ("marriage", "married", "wedding", "relationship", "love", "spouse", "partner", "divorce", "婚恋", "婚姻", "感情", "配偶", "恋爱", "结婚")): route = "relationship" focus_techniques = ["D9", "UL Upapada", "Dasha", "Nakshatra", "Vivah Saham"] elif any(k in q for k in ("money", "wealth", "finance", "investment", "property", "income", "财务", "财富", "投资", "房产", "收入")): route = "finance" focus_techniques = ["D2", "D11", "Dasha", "Shadbala", "Ashtakavarga"] elif any(k in q for k in ("when", "timing", "event", "prediction", "future", "应期", "预测", "何时", "将来")): route = "timing" focus_techniques = ["Dasha", "Transit", "Double Transit", "Gochara"] else: route = "general" focus_techniques = ["D1", "D9", "Dasha", "Yoga", "Shadbala", "Ashtakavarga"] result = _run_engine("full-reading", { "year": year, "month": month, "day": day, "hour": hour, "minute": minute, "lat": lat, "lon": lon, "tz": tz, "age": age, "transit_date": transit_date, "node_mode": node_mode, }) if isinstance(result, dict) and "error" not in result: strict_evidence = _collect_strict_evidence(route, result) result["routing"] = { "question_type": route, "focus_techniques": focus_techniques, "note": ( f"Routed to '{route}' path. Focus on the listed techniques " f"for higher-confidence answers. Full reading included for context, " f"and strict evidence audit now reports confidence cap and missing links." ), } result["strict_workflow"] = strict_evidence return result # ============================================================================ # Resources # ============================================================================ @mcp.resource("jyotish://technique-registry") def technique_registry_resource() -> str: """Full technique registry as JSON.""" registry_path = os.path.join(SCRIPT_DIR, "references", "technique_registry.json") with open(registry_path, "r", encoding="utf-8") as f: return f.read() @mcp.resource("jyotish://quick-reference") def quick_reference_resource() -> str: """Quick reference guide for Jyotish concepts.""" qr_path = os.path.join(SCRIPT_DIR, "references", "quick-reference-guide.md") with open(qr_path, "r", encoding="utf-8") as f: return f.read() @mcp.resource("jyotish://audit-status") def audit_status_resource() -> str: """Current audit status as JSON.""" status = _audit_status() return json.dumps(status, indent=2, ensure_ascii=False) # ============================================================================ # Main # ============================================================================ if __name__ == "__main__": mcp.run()