IMPLEMENTATION_PLAN.md 14 KB

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.

# 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:

{"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

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

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

@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

@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

@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)