|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
|
|
|
|
|
import json
|
|
import json
|
|
|
import logging
|
|
import logging
|
|
|
-from datetime import timedelta
|
|
|
|
|
|
|
+from datetime import datetime, timedelta, timezone
|
|
|
from typing import Any
|
|
from typing import Any
|
|
|
|
|
|
|
|
from mcp import ClientSession
|
|
from mcp import ClientSession
|
|
@@ -24,12 +24,14 @@ def _payload_from_result(result: Any) -> dict[str, Any]:
|
|
|
"""Extract a dict payload from an MCP CallToolResult.
|
|
"""Extract a dict payload from an MCP CallToolResult.
|
|
|
|
|
|
|
|
Tries structuredContent first, then parses text content as JSON.
|
|
Tries structuredContent first, then parses text content as JSON.
|
|
|
|
|
+ If no JSON dict is found, returns error text from content items.
|
|
|
"""
|
|
"""
|
|
|
payload = getattr(result, "structuredContent", None)
|
|
payload = getattr(result, "structuredContent", None)
|
|
|
if isinstance(payload, dict):
|
|
if isinstance(payload, dict):
|
|
|
return payload
|
|
return payload
|
|
|
|
|
|
|
|
- for item in getattr(result, "content", []) or []:
|
|
|
|
|
|
|
+ content_items = getattr(result, "content", []) or []
|
|
|
|
|
+ for item in content_items:
|
|
|
text = getattr(item, "text", None)
|
|
text = getattr(item, "text", None)
|
|
|
if not isinstance(text, str) or not text.strip():
|
|
if not isinstance(text, str) or not text.strip():
|
|
|
continue
|
|
continue
|
|
@@ -39,9 +41,36 @@ def _payload_from_result(result: Any) -> dict[str, Any]:
|
|
|
continue
|
|
continue
|
|
|
if isinstance(decoded, dict):
|
|
if isinstance(decoded, dict):
|
|
|
return decoded
|
|
return decoded
|
|
|
|
|
+
|
|
|
|
|
+ # If no JSON dict found, collect any error text from content items
|
|
|
|
|
+ error_texts = []
|
|
|
|
|
+ for item in content_items:
|
|
|
|
|
+ text = getattr(item, "text", None)
|
|
|
|
|
+ if isinstance(text, str) and text.strip():
|
|
|
|
|
+ error_texts.append(text.strip())
|
|
|
|
|
+ if error_texts:
|
|
|
|
|
+ return {"error": error_texts[0]}
|
|
|
return {}
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _normalize_datetime(dt_str: str | None) -> str | None:
|
|
|
|
|
+ """Normalize a datetime string to UTC ISO format without timezone offset.
|
|
|
|
|
+
|
|
|
|
|
+ The ephemeris server expects UTC datetimes as plain ISO strings
|
|
|
|
|
+ (e.g. '1965-07-02T22:05:00') without timezone info.
|
|
|
|
|
+ """
|
|
|
|
|
+ if dt_str is None:
|
|
|
|
|
+ return None
|
|
|
|
|
+ try:
|
|
|
|
|
+ dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
|
|
|
|
|
+ if dt.tzinfo is not None:
|
|
|
|
|
+ dt = dt.astimezone(timezone.utc)
|
|
|
|
|
+ return dt.strftime("%Y-%m-%dT%H:%M:%S")
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ logger.warning(f"failed to normalize datetime: {dt_str}")
|
|
|
|
|
+ return dt_str
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
async def call_sky_state(
|
|
async def call_sky_state(
|
|
|
datetime: str | None = None,
|
|
datetime: str | None = None,
|
|
|
lat: float = 0.0,
|
|
lat: float = 0.0,
|
|
@@ -51,22 +80,16 @@ async def call_sky_state(
|
|
|
) -> dict[str, Any]:
|
|
) -> dict[str, Any]:
|
|
|
"""Call ephemeris-mcp:get_sky_state and return the result dict.
|
|
"""Call ephemeris-mcp:get_sky_state and return the result dict.
|
|
|
|
|
|
|
|
- Args:
|
|
|
|
|
- datetime: ISO 8601 datetime (UTC). Defaults to now.
|
|
|
|
|
- lat: Observer latitude in decimal degrees.
|
|
|
|
|
- lon: Observer longitude in decimal degrees.
|
|
|
|
|
- elevation: Observer elevation in meters.
|
|
|
|
|
- geocentric: If True, return geocentric positions.
|
|
|
|
|
-
|
|
|
|
|
- Returns:
|
|
|
|
|
- The full sky_state dict from ephemeris-mcp, or an error dict
|
|
|
|
|
- if the call fails.
|
|
|
|
|
|
|
+ Datetime is normalized to UTC ISO format (no timezone offset)
|
|
|
|
|
+ before sending to the ephemeris server.
|
|
|
"""
|
|
"""
|
|
|
|
|
+ # Normalize datetime to UTC ISO string
|
|
|
|
|
+ dt_str = _normalize_datetime(datetime)
|
|
|
url = EPHEMERIS_MCP_URL.strip()
|
|
url = EPHEMERIS_MCP_URL.strip()
|
|
|
if not url.endswith("/mcp/sse"):
|
|
if not url.endswith("/mcp/sse"):
|
|
|
url = url.rstrip("/") + "/mcp/sse"
|
|
url = url.rstrip("/") + "/mcp/sse"
|
|
|
|
|
|
|
|
- logger.info(f"calling ephemeris-mcp get_sky_state url={url} dt={datetime}")
|
|
|
|
|
|
|
+ logger.info(f"calling ephemeris-mcp get_sky_state url={url} dt={dt_str}")
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
async with sse_client(url, timeout=8.0, sse_read_timeout=8.0) as streams:
|
|
async with sse_client(url, timeout=8.0, sse_read_timeout=8.0) as streams:
|
|
@@ -77,7 +100,7 @@ async def call_sky_state(
|
|
|
result = await session.call_tool(
|
|
result = await session.call_tool(
|
|
|
"get_sky_state",
|
|
"get_sky_state",
|
|
|
{
|
|
{
|
|
|
- "datetime": datetime,
|
|
|
|
|
|
|
+ "datetime": dt_str,
|
|
|
"lat": lat,
|
|
"lat": lat,
|
|
|
"lon": lon,
|
|
"lon": lon,
|
|
|
"elevation": elevation,
|
|
"elevation": elevation,
|