# Astro MCP Implementation Plan > Based on: WISHLIST_ANALYSIS.md > Strategy: Expand existing tools rather than creating new ones where possible. > Each phase is independently testable. Stop after each phase for review. --- ## Phase 0: Fix Natal Chart Applying/Separating Bug **File**: `src/astro_mcp/tools.py`, `calculate_natal_chart` function The natal chart tool builds planet lines without `speed_lon`, then calls `compute_aspects` on bodies that also lack `speed_lon`. This means `applying` is always `None` for natal aspects. **Change**: When building the `aspect_bodies` list in `calculate_natal_chart`, include `speed_lon` from the raw ephemeris data. This requires keeping a lookup of `speed_lon` by body name from the raw bodies. ```python # Before (line ~170): aspect_bodies = [{"name": p["body"], "lon": p["absolute_lon"]} for p in planets] # After: speed_lookup = {b["body"]: b.get("speed_lon") for b in raw_bodies} aspect_bodies = [{"name": p["body"], "lon": p["absolute_lon"], "speed_lon": speed_lookup.get(p["body"])} for p in planets] ``` **Tests**: Add a test in `test_tools.py` that verifies `applying` is not `None` for natal chart aspects when planets have different speeds. **Effort**: ~5 lines changed. --- ## Phase 1: Chart Overview Post-Processing Functions **File**: `src/astro_mcp/astrology.py` (new functions) Add these pure functions that operate on the planet list returned by `calculate_natal_chart`: ### 1.1 `get_element_balance(planets) -> dict` Count planets by element. Each sign maps to an element: - Fire: Aries, Leo, Sagittarius - Earth: Taurus, Virgo, Capricornus - Air: Gemini, Libra, Aquarius - Water: Cancer, Scorpius, Pisces Returns: `{"fire": N, "earth": N, "air": N, "water": N, "percentages": {...}}` ### 1.2 `get_modality_balance(planets) -> dict` Count planets by modality: - Cardinal: Aries, Cancer, Libra, Capricornus - Fixed: Taurus, Leo, Scorpius, Aquarius - Mutable: Gemini, Virgo, Sagittarius, Pisces Returns: `{"cardinal": N, "fixed": N, "mutable": N, "percentages": {...}}` ### 1.3 `get_hemisphere_emphasis(planets) -> dict` Count planets by house ranges: - Upper (houses 7-12), Lower (1-6), East (10-3), West (4-9) Returns: `{"upper": N, "lower": N, "east": N, "west": N}` ### 1.4 `detect_stelliums(planets) -> list` Scan for 3+ planets in the same sign OR same house. Returns: `[{"type": "sign"|"house", "key": str, "planets": [str, ...]}, ...]` ### 1.5 `get_empty_houses(planets) -> list[int]` Return house numbers (1-12) with no planets. ### 1.6 `get_chart_ruler(ascendant_sign, planets) -> dict` Sign-to-ruler mapping: - Aries -> Mars, Taurus -> Venus, Gemini -> Mercury, Cancer -> Moon, - Leo -> Sun, Virgo -> Mercury, Libra -> Venus, Scorpius -> Pluto (traditional: Mars), - Sagittarius -> Jupiter, Capricornus -> Saturn, Aquarius -> Uranus (traditional: Saturn), - Pisces -> Neptune (traditional: Jupiter) Return the ruling planet's full data from the planets list. ### 1.7 `get_house_rulers(houses, planets) -> list` For each house cusp sign, find the ruling planet and its condition. ### 1.8 `group_planets_by_house(planets) -> dict` Returns: `{house_number: [planet_names]}` ### 1.9 `group_planets_by_sign(planets) -> dict` Returns: `{sign_name: [planet_names]}` ### 1.10 `get_house_type_counts(planets) -> dict` Angular (1,4,7,10), Succedent (2,5,8,11), Cadent (3,6,9,12). ### 1.11 `get_retrograde_planets(planets) -> list` Filter planets where `retrograde=True`, return with sign + house. ### 1.12 `get_nodal_axis(planets, houses) -> dict` Extract `true_node` from planets, compute South Node (opposite point). ### 1.13 `get_saturn_info(planets) -> dict` Extract Saturn from planets list with sign, house, retrograde. ### 1.14 `get_part_of_fortune(ascendant_lon, sun_lon, moon_lon) -> dict` Formula: `normalize_degrees(ascendant_lon + moon_lon - sun_lon)`. Return sign + degree. ### 1.15 `get_pluto_polarity_point(pluto_lon) -> dict` Formula: `normalize_degrees(pluto_lon + 180)`. Return sign + degree. **Tests**: Add all functions to `test_astrology.py` with known inputs. **Effort**: ~200 lines in astrology.py, ~150 lines in test_astrology.py. --- ## Phase 2: Aspect Pattern Detection **File**: `src/astro_mcp/astrology.py` ### 2.1 `detect_aspect_patterns(planets, aspects) -> list` Scan the aspect list for known patterns: **T-square**: Find an opposition (A-B). Check if any planet C forms a square to both A and B. Apex = C. **Grand Trine**: Find 3 planets where each pair is in trine (3 trines total). All 3 must be in the same element. **Grand Cross**: Find 4 planets forming 2 oppositions and 4 squares (each planet opposes one and squares two others). **Yod**: Find 2 planets in sextile. Check if any planet C forms a quincunx (150°) to both. Note: quincunx is not in the current aspect definitions -- add it first. **Add quincunx to ASPECT_DEFINITIONS**: ```python {"name": "quincunx", "angle": 150.0, "default_orb": 3.0, "symbol": "Qx"} ``` Returns: `[{"type": "T-square"|"Grand Trine"|"Grand Cross"|"Yod", "planets": [...], "apex": str|null, "modality": str|null, "element": str|null}]` **Tests**: Create synthetic planet configurations that form each pattern type and verify detection. **Effort**: ~150 lines in astrology.py, ~100 lines in tests. --- ## Phase 3: Chart Shape Detection **File**: `src/astro_mcp/astrology.py` ### 3.1 `detect_chart_shape(planets) -> dict` Analyze the angular distribution of planets to classify the chart shape: 1. Sort planets by longitude 2. Compute gaps between adjacent planets (including wraparound) 3. Find the largest gap 4. Classify: - **Bundle**: All planets within 120° arc (largest gap >= 240°) - **Bowl**: All planets within 180° arc (largest gap >= 180°) - **Bucket**: All planets within 240° arc, with one planet opposite (singleton) -- largest gap < 240° but > 120° - **Splash**: Planets distributed around the full 360° (largest gap < 120°) - **Locomotive**: Planets within ~240° with a "locomotive" planet at the start of the empty arc - **Seesaw**: Two clusters of planets roughly opposite each other - **Splay**: Three or more planet pairs/groups distributed around the chart Returns: `{"shape": str, "largest_gap": float, "gap_start": float, "gap_end": float}` **Tests**: Create synthetic planet configurations for each shape. **Effort**: ~80 lines in astrology.py, ~80 lines in tests. --- ## Phase 4: Expand calculate_natal_chart with Overview **File**: `src/astro_mcp/tools.py` ### 4.1 Add `include_overview` parameter to `calculate_natal_chart` ```python async def calculate_natal_chart( ...existing params..., include_overview: bool = False, include_patterns: bool = False, include_karmic: bool = False, top_n_aspects: int | None = None, ) -> dict[str, Any]: ``` When `include_overview=True`, add an `"overview"` key to the output containing: - element_balance, modality_balance, hemisphere_emphasis - stelliums, empty_houses - chart_ruler, house_rulers - planets_by_house, planets_by_sign - house_type_counts - retrograde_planets - lunar_phase (from ephemeris `lunar_state`) When `include_patterns=True`, add an `"aspect_patterns"` key with T-square, Grand Trine, Grand Cross, Yod, and chart_shape. When `include_karmic=True`, add a `"karmic"` key with: - nodal_axis, saturn_info, pluto_polarity_point - nodal_aspects (filter aspects for true_node) - saturn_aspects (filter aspects for Saturn hard aspects to personal planets) - retrograde_planets (emphasize personal planet retrogrades) - 12th_house_analysis (cusp sign, planets, ruler) When `top_n_aspects` is set, limit the aspects list to the N tightest. **Tests**: Add tests in `test_tools.py` verifying each flag produces the expected output sections. **Effort**: ~100 lines in tools.py, ~150 lines in tests. --- ## Phase 5: Expand calculate_synastry_chart **File**: `src/astro_mcp/tools.py` ### 5.1 Add parameters to `calculate_synastry_chart` ```python async def calculate_synastry_chart( ...existing params..., top_n_aspects: int | None = None, karmic_filter: bool = False, significator_filter: bool = False, ) -> dict[str, Any]: ``` - `top_n_aspects`: Limit interaspects to top N by orb - `karmic_filter`: Only return interaspects involving Saturn, Pluto, or true_node - `significator_filter`: Only return interaspects involving Venus-Mars, Moon-Venus, Sun-Moon, Sun-Saturn pairs Add a `"summary"` key to the output containing: - `top_aspects`: Top 10 interaspects by orb - `saturn_contacts`: All Saturn interchart aspects - `node_contacts`: All Node interchart aspects - `venus_mars_contacts`: All Venus-Mars interchart aspects - `sun_moon_contacts`: All Sun-Moon interchart aspects **Tests**: Add tests verifying filters and summary sections. **Effort**: ~60 lines in tools.py, ~100 lines in tests. --- ## Phase 6: Full Davison Chart Tool **File**: `src/astro_mcp/tools.py` ### 6.1 New tool: `calculate_davison_chart` ```python @mcp.tool() async def calculate_davison_chart( person1_datetime: str, person1_latitude: float, person1_longitude: float, person2_datetime: str, person2_latitude: float, person2_longitude: float, elevation: float = 0.0, house_system: str = "placidus", orb_limits: dict[str, float] | None = None, ) -> dict[str, Any]: ``` Algorithm: 1. Compute Davison midpoint datetime (same as existing `compute_davison_chart`) 2. Compute Davison midpoint lat/lon (average of both birth locations) 3. Call `call_sky_state` at the Davison midpoint datetime + location 4. Calculate houses from the Davison LST + latitude 5. Build planet list with house placement 6. Calculate aspects between Davison planets 7. Calculate angles 8. Return full chart structure (same format as `calculate_natal_chart`) ### 6.2 New tool: `calculate_davison_chart_by_id` Same as above but accepts `person1_id` and `person2_id` from the database. ### 6.3 Update `calculate_synastry_chart` Change the Davison section from returning just midpoint coordinates to returning the full chart. Or alternatively, keep the midpoint data and add a `"davison_chart_full"` key. **Recommendation**: Keep the existing midpoint data (for backward compatibility) and add a `include_davison_full: bool = False` parameter. When True, compute and include the full Davison chart. **Tests**: Add tests for Davison chart calculation with known birth data. **Effort**: ~150 lines in tools.py, ~100 lines in tests. --- ## Phase 7: Transit Tools for Composite/Davison **File**: `src/astro_mcp/tools.py` ### 7.1 New tool: `get_composite_transit_preview` ```python @mcp.tool() async def get_composite_transit_preview( person1_datetime: str, person1_latitude: float, person1_longitude: float, person2_datetime: str, person2_latitude: float, person2_longitude: float, start_date: str, end_date: str, min_significance: float = 0.0, ) -> dict[str, Any]: ``` Algorithm: 1. Calculate composite chart (reuse `calculate_composite_chart`) 2. For each day in range, get transiting planet positions 3. Compute aspects between transiting planets and composite planet positions 4. Return daily snapshots (same format as `get_transit_preview`) ### 7.2 New tool: `get_davison_transit_preview` Same pattern but using Davison chart positions as the "natal" targets. ### 7.3 `_byId` variants Add `get_composite_transit_preview_by_id` and `get_davison_transit_preview_by_id`. **Tests**: Add tests for composite and Davison transit previews. **Effort**: ~150 lines in tools.py, ~100 lines in tests. --- ## Phase 8: Return Type Updates **File**: `src/astro_mcp/tools.py` ### 8.1 Update `_tool_names()` Add all new tools to the `_tool_names()` function in `server.py`: - `calculate_davison_chart` - `calculate_davison_chart_by_id` - `get_composite_transit_preview` - `get_composite_transit_preview_by_id` - `get_davison_transit_preview` - `get_davison_transit_preview_by_id` ### 8.2 Update root endpoint test Update `test_root_lists_tools` in `test_server.py` to include the new tool names. --- ## Phase 9: Karmic Relationship Synthesis **File**: `src/astro_mcp/tools.py` ### 9.1 New tool: `get_karmic_relationship_summary` ```python @mcp.tool() async def get_karmic_relationship_summary( person1_id: str, person2_id: str, house_system: str = "placidus", ) -> dict[str, Any]: ``` Algorithm: 1. Get synastry chart (with karmic_filter=True) 2. Get composite chart 3. Get Davison chart 4. Extract karmic indicators: - Saturn-Node contacts (synastry) - Pluto-Node contacts (synastry) - Node conjunctions (synastry) - Saturn personal planet contacts (synastry) - Composite Saturn/Pluto/Node positions - Davison Saturn/Pluto/Node positions 5. Return structured summary with karmic weight assessment **Effort**: ~100 lines in tools.py, ~50 lines in tests. --- ## Phase 10: Nice-to-Have (Deferred) These are lower priority and can be done after the above phases are complete: 1. **Vertex axis** (~25 lines in astrology.py) 2. **Fixed star conjunctions** (~100 lines + star catalog data) 3. **Coalescent chart** (~150 lines, complex algorithm) 4. **Relationship type classifier** (~80 lines, heuristic) 5. **Compatibility scoring** (~100 lines, heuristic) 6. **Eclipse proximity** (needs external eclipse data source) 7. **Planetary nodes** (needs specialized ephemeris data) --- ## Summary of Changes | Phase | Files Changed | New Lines (approx) | New Tools | |---|---|---|---| | 0: Bug fix | tools.py | ~5 | 0 | | 1: Post-processing | astrology.py, test_astrology.py | ~350 | 0 | | 2: Aspect patterns | astrology.py, test_astrology.py | ~250 | 0 | | 3: Chart shape | astrology.py, test_astrology.py | ~160 | 0 | | 4: Natal overview | tools.py, test_tools.py | ~250 | 0 (params) | | 5: Synastry filters | tools.py, test_tools.py | ~160 | 0 (params) | | 6: Davison chart | tools.py, test_tools.py, server.py | ~250 | 2 | | 7: Composite/Davison transits | tools.py, test_tools.py, server.py | ~250 | 4 | | 8: Return types | server.py, test_server.py | ~20 | 0 | | 9: Karmic synthesis | tools.py, test_tools.py | ~150 | 1 | | **Total** | | **~1,845** | **7** | --- ## Testing Strategy - All new astrology functions get unit tests in `test_astrology.py` - All new tool behaviors get integration tests in `test_tools.py` (with mocked ephemeris) - The existing 103 tests must continue to pass - Run `pytest tests/` after each phase ## Verification Checklist After all phases: - [ ] `pytest tests/` passes with no failures - [ ] All new functions have type annotations - [ ] All new tools appear in `GET /` output - [ ] All new tools have docstrings - [ ] No breaking changes to existing tool signatures (new params have defaults)