فهرست منبع

Add _byId tool variants for database-backed chart calculations

New tools:
- calculate_natal_chart_by_id(person_id, ...)
- calculate_transit_chart_by_id(person_id, transit_datetime, ...)
- calculate_synastry_chart_by_id(person1_id, person2_id, ...)
- calculate_composite_chart_by_id(person1_id, person2_id, ...)
- get_transit_preview_by_id(person_id, start_date, end_date, ...)

Each _byId tool fetches birth data from the persons DB and delegates
to the core chart tool. Optional params (transit_latitude, transit_longitude,
house_system, orb_limits, event_types) override defaults.

Also: fix calculate_transit_chart description (was missing transit_longitude param docs)

Tests: 103 passing (3 new _byId tests)
Lukas Goldschmidt 1 ماه پیش
والد
کامیت
15ae343e53
5فایلهای تغییر یافته به همراه299 افزوده شده و 6 حذف شده
  1. 36 6
      README.md
  2. 5 0
      src/astro_mcp/server.py
  3. 204 0
      src/astro_mcp/tools.py
  4. 5 0
      tests/test_server.py
  5. 49 0
      tests/test_tools.py

+ 36 - 6
README.md

@@ -29,17 +29,47 @@ SSE transport at `http://localhost:7016/mcp/sse`
 
 ## Tools
 
+### Core tools (raw birth data)
+
 | Tool | Description |
 |---|---|
 | `get_planetary_positions` | Planetary positions with zodiac signs, degrees, retrograde flags |
-| `calculate_natal_chart` | Complete natal chart (planets, houses, aspects, angles) |
-| `calculate_transit_chart` | Transit chart with natal-to-transit aspects + transit location |
-| `calculate_synastry_chart` | Relationship chart (interaspects, overlays, composite, davison) |
-| `calculate_composite_chart` | Composite chart via midpoint method (planets, houses, aspects, angles) |
-| `get_transit_preview` | Significant transit events for a person over a time range |
-| `person_manage` | CRUD for persons database (name, nickname, birth datetime/location/place, lat/lon) |
+| `calculate_natal_chart` | Complete natal chart (planets, houses, aspects, angles). Params: birth_datetime, latitude, longitude |
+| `calculate_transit_chart` | Transit chart with natal-to-transit aspects. Params: birth_datetime, transit_datetime, latitude, longitude, transit_latitude, transit_longitude |
+| `calculate_synastry_chart` | Relationship chart for two people. Params: person1_datetime/lat/lon, person2_datetime/lat/lon |
+| `calculate_composite_chart` | Composite chart via midpoint method. Params: same as synastry |
+| `get_transit_preview` | Significant transit events for a person ID over a time range |
+| `person_manage` | CRUD for persons database (name, nickname, birthplace, birth_datetime, lat/lon, elevation) |
 | `list_house_systems` | List supported house systems |
 
+### _byId tools (database-backed)
+
+Each core chart tool has a `_byId` variant that accepts a `person_id` (from the persons
+database) instead of raw birth data. All other optional parameters override the defaults.
+
+| Tool | Description |
+|---|---|
+| `calculate_natal_chart_by_id` | Natal chart by person_id |
+| `calculate_transit_chart_by_id` | Transit chart by person_id + transit_datetime |
+| `calculate_synastry_chart_by_id` | Synastry chart for person1_id + person2_id |
+| `calculate_composite_chart_by_id` | Composite chart for person1_id + person2_id |
+| `get_transit_preview_by_id` | Transit preview by person_id + date range |
+
+### Person database
+
+`person_manage` supports actions: `add`, `get` (by id or nickname), `list`, `update`, `delete`.
+
+Example workflow:
+```
+person_manage(action="add", name="Me", birth_datetime="1965-07-02T00:05:00+02:00",
+              birthplace="Graz, Austria", latitude=47.076668, longitude=15.421371)
+=> {"person": {"id": "abc12345", ...}}
+
+# Then use the _byId tools:
+calculate_natal_chart_by_id(person_id="abc12345")
+calculate_transit_chart_by_id(person_id="abc12345", transit_datetime="2026-06-02T12:00:00")
+```
+
 ## Dashboard
 
 Person management dashboard at `http://localhost:7016/dashboard`

+ 5 - 0
src/astro_mcp/server.py

@@ -31,6 +31,11 @@ def _tool_names() -> list[str]:
         "get_transit_preview",
         "person_manage",
         "list_house_systems",
+        "calculate_natal_chart_by_id",
+        "calculate_transit_chart_by_id",
+        "calculate_synastry_chart_by_id",
+        "calculate_composite_chart_by_id",
+        "get_transit_preview_by_id",
     ]
 
 

+ 204 - 0
src/astro_mcp/tools.py

@@ -916,3 +916,207 @@ def list_house_systems() -> dict[str, Any]:
             {"id": "whole_sign", "name": "Whole Sign", "description": "Each house corresponds to one full sign. The ASC sign is house 1."},
         ]
     }
+
+
+# ── _byId convenience tools ──────────────────────────────────────────
+# These tools accept a person_id (from the DB) and optional overrides,
+# then call the core chart tools with the person's birth data.
+# Unprovided optional params fall back to the default of the core tool.
+
+
+async def _get_person_birth_data(person_id: str) -> dict[str, Any]:
+    """Fetch birth data from DB or return error dict."""
+    from . import storage
+    person = await storage.get_person(person_id=person_id)
+    if not person:
+        return {"error": f"person not found: {person_id}"}
+    return {
+        "birth_datetime": person["birth_datetime"],
+        "birthplace": person.get("birthplace"),
+        "latitude": person["latitude"],
+        "longitude": person["longitude"],
+        "elevation": person.get("elevation", 0.0),
+    }
+
+
+@mcp.tool()
+async def calculate_natal_chart_by_id(
+    person_id: str,
+    transit_latitude: float | None = None,
+    transit_longitude: float | None = None,
+    house_system: str = "placidus",
+    orb_limits: dict[str, float] | None = None,
+) -> dict[str, Any]:
+    """Calculate natal chart for a person from the database.
+
+    Args:
+        person_id: ID of a person in the persons database.
+        transit_latitude: Override latitude for chart calculation. Defaults to birth latitude.
+        transit_longitude: Override longitude for chart calculation. Defaults to birth longitude.
+        house_system: House system (default: Placidus).
+        orb_limits: Optional orb configuration.
+
+    Returns:
+        Complete natal chart structure.
+    """
+    birth = await _get_person_birth_data(person_id)
+    if "error" in birth:
+        return birth
+    return await calculate_natal_chart(
+        birth_datetime=birth["birth_datetime"],
+        latitude=transit_latitude if transit_latitude is not None else birth["latitude"],
+        longitude=transit_longitude if transit_longitude is not None else birth["longitude"],
+        elevation=birth.get("elevation", 0.0),
+        house_system=house_system,
+        orb_limits=orb_limits,
+    )
+
+
+@mcp.tool()
+async def calculate_transit_chart_by_id(
+    person_id: str,
+    transit_datetime: str,
+    transit_latitude: float | None = None,
+    transit_longitude: float | None = None,
+    house_system: str = "placidus",
+    orb_limits: dict[str, float] | None = None,
+) -> dict[str, Any]:
+    """Calculate transit chart for a person from the database.
+
+    Args:
+        person_id: ID of a person in the persons database.
+        transit_datetime: ISO 8601 transit datetime (UTC).
+        transit_latitude: Current location latitude. Defaults to birth latitude.
+        transit_longitude: Current location longitude. Defaults to birth longitude.
+        house_system: House system for natal houses (default: Placidus).
+        orb_limits: Optional orb configuration.
+
+    Returns:
+        Transit chart structure.
+    """
+    birth = await _get_person_birth_data(person_id)
+    if "error" in birth:
+        return birth
+    return await calculate_transit_chart(
+        birth_datetime=birth["birth_datetime"],
+        transit_datetime=transit_datetime,
+        latitude=birth["latitude"],
+        longitude=birth["longitude"],
+        transit_latitude=transit_latitude,
+        transit_longitude=transit_longitude,
+        elevation=birth.get("elevation", 0.0),
+        house_system=house_system,
+        orb_limits=orb_limits,
+    )
+
+
+@mcp.tool()
+async def calculate_synastry_chart_by_id(
+    person1_id: str,
+    person2_id: str,
+    house_system: str = "placidus",
+    orb_limits: dict[str, float] | None = None,
+) -> dict[str, Any]:
+    """Calculate synastry chart for two persons from the database.
+
+    Args:
+        person1_id: ID of person 1 in the persons database.
+        person2_id: ID of person 2 in the persons database.
+        house_system: House system (default: Placidus).
+        orb_limits: Optional orb configuration.
+
+    Returns:
+        Synastry chart structure.
+    """
+    p1 = await _get_person_birth_data(person1_id)
+    if "error" in p1:
+        return p1
+    p2 = await _get_person_birth_data(person2_id)
+    if "error" in p2:
+        return p2
+    return await calculate_synastry_chart(
+        person1_datetime=p1["birth_datetime"],
+        person1_latitude=p1["latitude"],
+        person1_longitude=p1["longitude"],
+        person2_datetime=p2["birth_datetime"],
+        person2_latitude=p2["latitude"],
+        person2_longitude=p2["longitude"],
+        elevation=p1.get("elevation", 0.0),
+        house_system=house_system,
+        orb_limits=orb_limits,
+    )
+
+
+@mcp.tool()
+async def calculate_composite_chart_by_id(
+    person1_id: str,
+    person2_id: str,
+    house_system: str = "placidus",
+    orb_limits: dict[str, float] | None = None,
+) -> dict[str, Any]:
+    """Calculate composite chart for two persons from the database.
+
+    Args:
+        person1_id: ID of person 1 in the persons database.
+        person2_id: ID of person 2 in the persons database.
+        house_system: House system (default: Placidus).
+        orb_limits: Optional orb configuration.
+
+    Returns:
+        Composite chart structure.
+    """
+    p1 = await _get_person_birth_data(person1_id)
+    if "error" in p1:
+        return p1
+    p2 = await _get_person_birth_data(person2_id)
+    if "error" in p2:
+        return p2
+    return await calculate_composite_chart(
+        person1_datetime=p1["birth_datetime"],
+        person1_latitude=p1["latitude"],
+        person1_longitude=p1["longitude"],
+        person2_datetime=p2["birth_datetime"],
+        person2_latitude=p2["latitude"],
+        person2_longitude=p2["longitude"],
+        elevation=p1.get("elevation", 0.0),
+        house_system=house_system,
+        orb_limits=orb_limits,
+    )
+
+
+@mcp.tool()
+async def get_transit_preview_by_id(
+    person_id: str,
+    start_date: str,
+    end_date: str,
+    transit_latitude: float | None = None,
+    transit_longitude: float | None = None,
+    event_types: list[str] | None = None,
+    orb_limits: dict[str, float] | None = None,
+) -> dict[str, Any]:
+    """Preview transit events for a person from the database.
+
+    Args:
+        person_id: ID of a person in the persons database.
+        start_date: ISO date string for the start of the range.
+        end_date: ISO date string for the end of the range.
+        transit_latitude: Current location latitude. Defaults to birth latitude.
+        transit_longitude: Current location longitude. Defaults to birth longitude.
+        event_types: Optional filter for event types.
+        orb_limits: Optional orb configuration.
+
+    Returns:
+        List of transit events.
+    """
+    birth = await _get_person_birth_data(person_id)
+    if "error" in birth:
+        return birth
+    return await get_transit_preview(
+        person_id=person_id,
+        start_date=start_date,
+        end_date=end_date,
+        transit_latitude=transit_latitude if transit_latitude is not None else birth["latitude"],
+        transit_longitude=transit_longitude if transit_longitude is not None else birth["longitude"],
+        event_types=event_types,
+        orb_limits=orb_limits,
+    )

+ 5 - 0
tests/test_server.py

@@ -29,5 +29,10 @@ def test_root_lists_tools() -> None:
         "get_transit_preview",
         "person_manage",
         "list_house_systems",
+        "calculate_natal_chart_by_id",
+        "calculate_transit_chart_by_id",
+        "calculate_synastry_chart_by_id",
+        "calculate_composite_chart_by_id",
+        "get_transit_preview_by_id",
     ]
     assert data["mcp"] == {"sse": "/mcp/sse", "messages": "/mcp/messages"}

+ 49 - 0
tests/test_tools.py

@@ -508,3 +508,52 @@ class TestTransitPreview:
             )
         )
         assert "error" in result
+
+
+# ── Tests: _byId tools ───────────────────────────────────────────────
+
+class TestByIdTools:
+    def test_calculate_natal_chart_by_id(self, tmp_path, monkeypatch):
+        from src.astro_mcp import tools, storage
+        db_path = tmp_path / "test.sqlite3"
+        monkeypatch.setattr(storage, "DB_PATH", db_path)
+        storage._initialized = False
+
+        import asyncio
+        pid = asyncio.run(tools.person_manage(
+            action="add", name="Lucky",
+            birth_datetime="1980-06-15T10:30:00",
+            latitude=47.0, longitude=8.0,
+            nickname="lucky", birthplace="Zurich",
+        ))["person"]["id"]
+
+        result = asyncio.run(tools.calculate_natal_chart_by_id(person_id=pid))
+        assert "error" not in result
+        assert result["chart_type"] == "natal"
+        assert len(result["planets"]) == 13
+
+    def test_calculate_transit_chart_by_id(self, tmp_path, monkeypatch):
+        from src.astro_mcp import tools, storage
+        db_path = tmp_path / "test2.sqlite3"
+        monkeypatch.setattr(storage, "DB_PATH", db_path)
+        storage._initialized = False
+
+        import asyncio
+        pid = asyncio.run(tools.person_manage(
+            action="add", name="Transit",
+            birth_datetime="1990-03-20T08:00:00",
+            latitude=52.0, longitude=13.0,
+            nickname="transit_test", birthplace="Berlin",
+        ))["person"]["id"]
+
+        result = asyncio.run(tools.calculate_transit_chart_by_id(
+            person_id=pid, transit_datetime="2026-06-02T12:00:00",
+        ))
+        assert "error" not in result
+        assert result["chart_type"] == "transit"
+
+    def test_by_id_not_found(self):
+        from src.astro_mcp import tools
+        import asyncio
+        result = asyncio.run(tools.calculate_natal_chart_by_id(person_id="nonexistent"))
+        assert "error" in result