server.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. from __future__ import annotations
  2. import logging
  3. from pathlib import Path
  4. from fastapi import FastAPI, HTTPException
  5. from fastapi.responses import Response
  6. from fastapi.staticfiles import StaticFiles
  7. from fastapi.templating import Jinja2Templates
  8. from mcp.server.fastmcp import FastMCP
  9. from mcp.server.fastmcp.prompts.base import Message
  10. from mcp.server.transport_security import TransportSecuritySettings
  11. from . import config
  12. from . import __version__
  13. logger = logging.getLogger("astro-mcp")
  14. mcp = FastMCP(
  15. "astro-mcp",
  16. transport_security=TransportSecuritySettings(
  17. enable_dns_rebinding_protection=False,
  18. ),
  19. )
  20. # Import tools module to register all @mcp.tool() handlers
  21. from . import tools # noqa: E402, F401
  22. from . import chart_resources # noqa: E402, F401
  23. # Templates and static files
  24. TEMPLATES_DIR = Path(__file__).resolve().parent.parent.parent / "templates"
  25. STATIC_DIR = Path(__file__).resolve().parent.parent.parent / "static"
  26. GUIDES_DIR = Path(__file__).resolve().parent.parent.parent / "agent-guides"
  27. templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
  28. def _tool_names() -> list[str]:
  29. return [
  30. "get_planetary_positions",
  31. "calculate_natal_chart",
  32. "calculate_transit_chart",
  33. "calculate_synastry_chart",
  34. "calculate_composite_chart",
  35. "calculate_davison_chart",
  36. "get_transit_preview",
  37. "get_composite_transit_preview",
  38. "get_davison_transit_preview",
  39. "get_karmic_relationship_summary",
  40. "person_manage",
  41. "list_house_systems",
  42. "calculate_natal_chart_by_id",
  43. "calculate_transit_chart_by_id",
  44. "calculate_synastry_chart_by_id",
  45. "calculate_composite_chart_by_id",
  46. "calculate_davison_chart_by_id",
  47. "get_transit_preview_by_id",
  48. ]
  49. def _read_guide(filename: str) -> str:
  50. """Read an agent guide markdown file."""
  51. path = GUIDES_DIR / filename
  52. if path.exists():
  53. return path.read_text(encoding="utf-8")
  54. return f"# Error\n\nGuide not found: {filename}"
  55. @mcp.resource("astro://guides/natal-astrology")
  56. def natal_astrology_guide() -> str:
  57. """Agent interpretation guide for natal chart analysis."""
  58. return _read_guide("natal-astrology.md")
  59. @mcp.resource("astro://guides/karmic-astrology")
  60. def karmic_astrology_guide() -> str:
  61. """Agent interpretation guide for karmic chart analysis."""
  62. return _read_guide("karmic-astrology.md")
  63. @mcp.resource("astro://guides/relationship-astrology")
  64. def relationship_astrology_guide() -> str:
  65. """Agent interpretation guide for relationship chart analysis."""
  66. return _read_guide("relationship-astrology.md")
  67. @mcp.resource("astro://guides/financial-astrology")
  68. def financial_astrology_guide() -> str:
  69. """Agent interpretation guide for financial astrology — business cycles, stock market forecasting, planetary correlations."""
  70. return _read_guide("financial-astrology.md")
  71. @mcp.resource("astro://guides/server-guide")
  72. def server_guide() -> str:
  73. """Server usage guide — architecture, timezone rules, person management, tool reference."""
  74. return _read_guide("server-guide.md")
  75. # ── Prompts ──────────────────────────────────────────────────────────
  76. def _msg(role: str, content: str) -> Message:
  77. return Message(content=content, role=role)
  78. @mcp.prompt()
  79. def natal_reading(person_id: str) -> list[Message]:
  80. """Full natal chart reading workflow — Big Three, elements, aspects, karmic indicators."""
  81. return [
  82. _msg("user", (
  83. f"Please perform a complete natal chart reading for person {person_id}.\n\n"
  84. "Steps:\n"
  85. f"1. Call `calculate_natal_chart_by_id` with person_id=`{person_id}`, "
  86. "include_overview=true, include_patterns=true, include_karmic=true\n"
  87. "2. Fetch the guide at `astro://guides/natal-astrology`\n"
  88. "3. Interpret using the methodology in that guide\n"
  89. "4. Structure your output as described below"
  90. )),
  91. _msg("assistant", (
  92. "I'll structure the reading as follows:\n"
  93. "- **The Big Three** — Sun (core identity), Moon (emotional nature), Ascendant (outer expression)\n"
  94. "- **Elemental & Modal Balance** — dominant element and modality\n"
  95. "- **Chart Ruler** — the ruler of the Ascendant and its condition\n"
  96. "- **Key Planetary Aspects** — up to 5 significant aspects with interpretation\n"
  97. "- **Aspect Patterns** — any T-squares, Grand Trines, Grand Crosses, Yods\n"
  98. "- **Karmic Indicators** — nodal axis, Saturn, Pluto Polarity Point\n"
  99. "- **Summary** — a synthesized paragraph integrating all themes\n\n"
  100. "Fetching chart data now..."
  101. )),
  102. ]
  103. @mcp.prompt()
  104. def karmic_reading(person_id: str) -> list[Message]:
  105. """Karmic-focused natal reading — nodal axis, Saturn, Pluto, retrogrades, 12th house."""
  106. return [
  107. _msg("user", (
  108. f"Please perform a karmic natal reading for person {person_id}.\n\n"
  109. "Steps:\n"
  110. f"1. Call `calculate_natal_chart_by_id` with person_id=`{person_id}`, "
  111. "include_overview=true, include_karmic=true\n"
  112. "2. Fetch the guide at `astro://guides/karmic-astrology`\n"
  113. "3. Interpret using the karmic methodology in that guide\n"
  114. "4. Structure your output as described below"
  115. )),
  116. _msg("assistant", (
  117. "I'll approach this as a karmic reading, leading with the nodal axis and Saturn "
  118. "before addressing the Big Three. The chart will be read as a record of soul memory "
  119. "and current-life developmental themes.\n\n"
  120. "Structure:\n"
  121. "- **Nodal Axis** — South Node (past-life patterns), North Node (growth direction), house placement\n"
  122. "- **Saturn** — sign, house, hard aspects to personal planets (karmic lessons)\n"
  123. "- **Pluto & Polarity Point** — evolutionary desires and the balancing point\n"
  124. "- **Retrograde Planets** — unfinished past-life business\n"
  125. "- **12th House** — spiritual karma, unconscious patterns\n"
  126. "- **Synthesis** — the soul's core developmental arc in this lifetime\n\n"
  127. "Fetching chart data now..."
  128. )),
  129. ]
  130. @mcp.prompt()
  131. def synastry_reading(person1_id: str, person2_id: str) -> list[Message]:
  132. """Relationship reading — synastry, composite, Davison, karmic overlay."""
  133. return [
  134. _msg("user", (
  135. f"Please perform a relationship reading for person {person1_id} and person {person2_id}.\n\n"
  136. "Steps:\n"
  137. f"1. Call `calculate_synastry_chart_by_id` with person1_id=`{person1_id}`, "
  138. f"person2_id=`{person2_id}`, include_davison_full=true\n"
  139. f"2. Call `calculate_composite_chart_by_id` with person1_id=`{person1_id}`, "
  140. f"person2_id=`{person2_id}`\n"
  141. "3. Fetch the guide at `astro://guides/relationship-astrology`\n"
  142. "4. Interpret using the methodology in that guide\n"
  143. "5. Structure your output as described below"
  144. )),
  145. _msg("assistant", (
  146. "I'll structure the relationship reading as follows:\n"
  147. "- **Synastry Overview** — top interchart aspects (tightest orbs), Venus-Mars chemistry, Sun-Moon compatibility\n"
  148. "- **Saturn & Node Contacts** — commitment patterns, karmic bonds, fated connections\n"
  149. "- **House Overlays** — which life areas each person activates in the other\n"
  150. "- **Composite Chart** — the relationship as its own entity (Sun, Moon, Ascendant, key aspects)\n"
  151. "- **Davison Chart** — inner emotional tone and long-term evolution\n"
  152. "- **Karmic Overlay** — Saturn-Node, Pluto-South Node contacts across charts\n"
  153. "- **Summary** — core narrative, strengths, growth edges\n\n"
  154. "Fetching chart data now..."
  155. )),
  156. ]
  157. @mcp.prompt()
  158. def transit_check(person_id: str, start_date: str, end_date: str) -> str:
  159. """Transit preview workflow — what's activating a person's natal chart in a date range.
  160. Args:
  161. person_id: Person identifier (ID or nickname).
  162. start_date: Start of the transit window (ISO 8601, e.g. '2026-06-01').
  163. end_date: End of the transit window (ISO 8601, e.g. '2026-06-30').
  164. """
  165. return (
  166. f"Please perform a transit analysis for person {person_id} "
  167. f"from {start_date} to {end_date}.\n\n"
  168. "Steps:\n"
  169. f"1. Call `get_transit_preview_by_id` with person_id=`{person_id}`, "
  170. f"start=`{start_date}`, end=`{end_date}`\n"
  171. "2. Fetch the guide at `astro://guides/natal-astrology` (see the transit section)\n"
  172. "3. Focus on transits to Sun, Moon, Ascendant (personal activation)\n"
  173. "4. Note Saturn transits (life structure changes) and Jupiter transits (growth opportunities)\n"
  174. "5. Highlight any exact aspects (orb <= 1°) as peak activation dates\n\n"
  175. "Structure:\n"
  176. "- **Overview** — dominant transit themes for the period\n"
  177. "- **Key Dates** — exact aspects with dates and interpretation\n"
  178. "- **Saturn/Jupiter Transits** — structural and growth-oriented movements\n"
  179. "- **Summary** — what the person is likely experiencing and how to work with it"
  180. )
  181. @mcp.prompt()
  182. def financial_forecast(target_date: str | None = None) -> str:
  183. """Financial astrology workflow — market sentiment, planetary cycles, Solar Ingress method.
  184. Args:
  185. target_date: Optional focal date (ISO 8601). Defaults to today if not provided.
  186. """
  187. date_line = f"Focus the analysis on {target_date}." if target_date else \
  188. "Use today's date as the focal point."
  189. return (
  190. "You are performing a financial astrology analysis using this server's tools.\n\n"
  191. f"{date_line}\n\n"
  192. "Steps:\n"
  193. "1. Call `get_planetary_positions` for the target date\n"
  194. "2. Fetch the guide at `astro://guides/financial-astrology`\n"
  195. "3. Identify major aspect patterns (Jupiter-Saturn, Jupiter-Uranus, Saturn-Uranus)\n"
  196. "4. Check the Moon's North Node position for McWhirter cycle timing\n"
  197. "5. Apply the Solar Ingress method for quarterly market direction\n"
  198. "6. Synthesize into a market sentiment outlook\n\n"
  199. "Structure:\n"
  200. "- **Active Cycles** — which planetary cycles are currently in play\n"
  201. "- **Key Aspects** — major configurations and their financial significance\n"
  202. "- **Market Sentiment** — bullish/bearish/neutral based on aspect balance\n"
  203. "- **Timing Windows** — dates of exact aspects as potential turning points\n\n"
  204. "Do not make specific investment recommendations. Frame findings as energetic tendencies."
  205. )
  206. def create_app() -> FastAPI:
  207. config.LOG_DIR.mkdir(parents=True, exist_ok=True)
  208. logging.basicConfig(
  209. filename=str(config.LOG_DIR / "server.log"),
  210. level=logging.INFO,
  211. format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
  212. )
  213. app = FastAPI(title="astro-mcp")
  214. # Mount static files
  215. if STATIC_DIR.exists():
  216. app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
  217. # Mount MCP SSE
  218. app.mount("/mcp", mcp.sse_app())
  219. # Dashboard routes
  220. from .dashboard import router as dashboard_router # noqa: E402
  221. app.include_router(dashboard_router)
  222. @app.get("/health")
  223. def health() -> dict:
  224. return {"ok": True, "server": "astro-mcp", "version": __version__, "port": config.PORT}
  225. @app.get("/charts/natal/{person_id}.{format}")
  226. async def natal_chart_image(person_id: str, format: str, size: int = 600):
  227. """Return a database-backed natal chart using the existing renderer."""
  228. try:
  229. content, content_type = await chart_resources.render_natal_artifact(
  230. person_id, format=format, size=size,
  231. )
  232. return Response(content=content, media_type=content_type)
  233. except (LookupError, ValueError) as exc:
  234. raise HTTPException(status_code=404 if isinstance(exc, LookupError) else 400, detail=str(exc)) from exc
  235. @app.get("/")
  236. def root() -> dict:
  237. return {
  238. "server": "astro-mcp",
  239. "version": __version__,
  240. "status": "ready",
  241. "tools": _tool_names(),
  242. "mcp": {
  243. "sse": "/mcp/sse",
  244. "messages": "/mcp/messages",
  245. },
  246. }
  247. return app