Port: 7016 (ephemeris-mcp is 7015)
An MCP server that consumes ephemeris-mcp:get_sky_state via MCP-over-SSE client, then performs pure astrological calculations (zodiac signs, houses, aspects, angles) on top of that astronomical data. No interpretation -- only structured JSON output. Includes a person database (SQLite), a Jinja2+vanillaJS dashboard, and a Docker container.
astro-mcp/
├── Dockerfile
├── docker-compose.yml
├── main.py # entrypoint, same pattern as ephemeris-mcp
├── requirements.txt
├── .gitignore
├── README.md
├── PROJECT.md
├── run.sh
├── killserver.sh
├── restart.sh
├── tests.sh
├── data/ # SQLite DBs at runtime
├── logs/ # PID + server.log
├── src/
│ └── astro_mcp/
│ ├── __init__.py
│ ├── config.py # PORT=7016, dirs, ephemeris URL
│ ├── server.py # FastAPI + FastMCP + lifespan + HTTP routes
│ ├── ephemeris_client.py # MCP SSE client calling ephemeris-mcp:get_sky_state
│ ├── astrology.py # pure calc: zodiac, houses, aspects, angles
│ ├── models.py # Pydantic models / type aliases for chart structs
│ ├── storage.py # SQLite: persons table + chart cache
│ ├── dashboard.py # Jinja2 templates + dashboard routes
│ └── tools.py # @mcp.tool() definitions (wiring layer)
├── templates/
│ ├── base.html
│ ├── dashboard.html
│ └── persons.html
└── tests/
├── conftest.py
├── test_astrology.py # unit tests for pure calc functions
├── test_tools.py # integration tests with mocked ephemeris client
└── test_server.py # HTTP endpoint smoke tests
fastapi>=0.115.0
uvicorn[standard]>=0.30.0
fastmcp>=2.0.0
mcp>=1.0.0
pydantic>=2.8.0
jinja2>=3.1.0
python-dotenv>=1.0.1
pytest>=8.0.0
Note: No pyswisseph -- all astronomical data comes from ephemeris-mcp via MCP client. No heavy native deps beyond what FastMCP/mcp pulls in.
MCP client pattern: Same as hermes-mcp's argus_client.py -- sse_client(url) + ClientSession. The ephemeris URL is configurable via EPHEMERIS_MCP_URL env var (default http://ephemeris-mcp:7015/mcp/sse for Docker, http://127.0.0.1:7015/mcp/sse for local dev).
Async throughout: The ephemeris client is async (sse_client is async context manager). The FastMCP tools are async def. The SQLite access uses asyncio.to_thread() to avoid blocking.
Calculation module is pure sync: astrology.py has no async, no I/O -- just math on dicts/lists. Easy to unit test without any event loop.
Ephemeris client call_sky_state helper: A single internal async function that calls ephemeris-mcp:get_sky_state and returns the parsed dict. All tools go through this. Timeout: 8s.
House systems: Placidus default, with house_system parameter on chart tools. Start with Placidus + Equal House only. The house calculation uses the sidereal time from get_sky_state output (sidereal_time.local_sidereal_time).
Aspect orbs (defaults): conjunction 8, sextile 6, square 8, trine 8, opposition 8. Configurable per-tool via optional orb_limits dict.
Person DB schema:
CREATE TABLE persons (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
nickname TEXT UNIQUE,
birth_datetime TEXT NOT NULL, -- ISO 8601 UTC
latitude REAL NOT NULL,
longitude REAL NOT NULL,
elevation REAL DEFAULT 0.0,
created_at TEXT NOT NULL -- ISO 8601 UTC
);
Docker: Based on python:3.13-slim, no build-essential needed (no pyswisseph). Health check hits http://127.0.0.1:7016/health.
| Tool | Description |
|---|---|
get_planetary_positions |
Wrapper around ephemeris-mcp, adds sign + degree + retrograde. Optional bodies filter. |
calculate_natal_chart |
Full natal chart: planets in signs/houses, aspects (applying/separating), angles. Params: birth_datetime, lat, lon, optional house_system, orb_limits. |
calculate_transit_chart |
Transit planets + natal-to-transit aspects + transiting planets in natal houses. Same params as natal + transit_datetime. |
calculate_synastry_chart |
Two birth data sets -> interaspects, house overlays, composite chart (midpoint method), Davison chart. |
get_transit_preview |
Person ID + time range -> list of events (exact aspects, ingresses, stations, lunar phases). Optional event_types filter. |
person_manage |
action param: add, get, list, update, delete. Birth data in add/update. |
list_house_systems |
Discovery tool -- returns supported house systems. |
services:
astro-mcp:
build: {context: ., dockerfile: Dockerfile}
image: astro-mcp:latest
container_name: astro-mcp
restart: unless-stopped
ports: ["7016:7016"]
environment:
ASTRO_HOST: 0.0.0.0
ASTRO_PORT: 7016
ASTRO_DATA_DIR: /app/data
ASTRO_LOG_DIR: /app/logs
ASTRO_DB_PATH: /app/data/astro.sqlite3
EPHEMERIS_MCP_URL: http://ephemeris-mcp:7015/mcp/sse
volumes: ["./data:/app/data", "./logs:/app/logs"]
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:7016/health').read()"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
.gitignore, requirements.txt, src/astro_mcp/ packagemain.py entry point (copy ephemeris-mcp pattern)config.py (PORT=7016, dirs, EPHEMERIS_MCP_URL)server.py basic FastAPI + FastMCP with /health and /run.sh, killserver.sh, restart.sh, tests.sh./run.sh starts, curl /health returns ok, commitastrology.py: zodiac sign from ecliptic longitude, house cusps (Placidus + Equal), aspect detection with orbs, angle calculation (ASC/MC from sidereal time)tests/test_astrology.py: comprehensive unit testspytest tests/test_astrology.py -q passes, commitephemeris_client.py: call_sky_state(datetime, lat, lon) using sse_clienttools.py: get_planetary_positions tool -- calls sky_state, adds sign/degree/retrograde to each bodystorage.py: persons DB, same cache pattern as ephemeris-mcp but using asyncio.to_thread for DB accessperson_manage tool (add/get/list/update/delete)tests/test_tools.py: person CRUD testscalculate_natal_chart in tools.pycall_sky_state -> feeds planetary positions into astrology module -> assembles full chart dictcalculate_transit_chart: calls sky_state twice (natal + transit datetimes), computes aspects between themcalculate_synastry_chart: calls sky_state twice, computes interaspects, house overlays, composite (midpoint), Davisonget_transit_preview: Given person_id + start/end range, iterates through time steps to find exact aspects (orb <= 0d01), ingresses (sign changes), retrograde stations (speed_lon sign changes), lunar phase changesdashboard.py: Jinja2 templates mounted on FastAPI routes (/dashboard, /dashboard/persons, /dashboard/persons/<id>)server.py lifespan or at module leveltests.shREADME.md: tool descriptions, example calls, Docker usagePROJECT.md: technical specs, schema, architecture