Skip to content

API reference

The supporting Python surfaces — typed errors, the unit discipline, shared pydantic base schemas, and the on-disk cache. Tool functions and their request / response models are documented on the Tool reference page.

Errors

astrodynamics_mcp.errors

Typed AstrodynamicsMCPError hierarchy with stable string error codes.

Every error raised inside a tool's body must be one of these subclasses (or the root). The stable string code attribute is the wire-format contract LLM consumers can match on — a fresh code prefix means a new error category; extending a category means new dotted suffixes under an existing prefix.

The numeric JSON-RPC error code (e.g. -32602 for "Invalid params") is added by the FastMCP server layer when it converts these exceptions into MCP error envelopes; the taxonomy here is provider-agnostic.

AstrodynamicsMCPError

Bases: Exception

Root of the astrodynamics-mcp error hierarchy.

Direct instances are valid for genuinely uncategorised failures, but in practice every raise site should pick the most specific subclass and a dotted code under that subclass's prefix.

to_mcp_error
to_mcp_error() -> dict[str, Any]

Serialise to the dict that a transport layer puts on the wire.

The shape is {"code": str, "message": str, "data": dict} — the same JSON-serialisable triple the MCP error envelope's data slot carries when the FastMCP server layer wraps the exception. The code field is preserved bit-equal through json.dumps / loads.

InvalidInputError

Bases: AstrodynamicsMCPError

Raised before any upstream library call when an argument fails validation.

Use for: schema violations the pydantic layer didn't catch (e.g. semantic constraints like "epoch must include a time component"), unknown unit strings, out-of-range numeric inputs, unresolved named-entity inputs (e.g. unknown ground-station name).

UpstreamError

Bases: AstrodynamicsMCPError

Wraps a failure from a vetted upstream library.

Captures the original exception's type and message so the LLM consumer sees a stable error code plus enough context to decide whether to retry with different arguments or surface the problem to the user.

DataSourceError

Bases: AstrodynamicsMCPError

Network or upstream-API failure (CelesTrak, JPL Horizons, IERS, …).

CredentialRequiredError

Bases: AstrodynamicsMCPError

Forward-compatible placeholder for v0.2 credentialed tools.

Not raised at v0.1 — defined so the hierarchy is stable and downstream consumers can write except CredentialRequiredError ahead of time.

Units

astrodynamics_mcp.units

{value, unit} discipline helpers and the allowed-unit registry.

Every numeric value crossing the MCP wire goes through one of:

  • :func:quantity — scalars wrapped as {"value": <number>, "unit": <str>}
  • :func:quantity_vector — vectors wrapped as {"value": [<number>, ...], "unit": <str>}

The polymorphic value key (number or list-of-numbers) keeps the envelope consistent across scalar and vector outputs; each tool's pydantic output schema fixes the field's type per slot.

The :data:ALLOWED_UNITS registry is the closed set tools may emit. Unknown strings fail fast with :class:InvalidInputError so a typo at the tool layer surfaces as a typed error rather than as a silently malformed payload.

:class:Quantity and :class:QuantityVector are the pydantic models tool output schemas compose from; the JSON-schema produced from each is what the cross-tool unit-discipline meta-test checks every tool against.

ALLOWED_UNITS module-attribute

ALLOWED_UNITS: frozenset[str] = frozenset(
    {
        "1",
        "m",
        "km",
        "AU",
        "m/s",
        "km/s",
        "rad",
        "deg",
        "s",
        "min",
        "hours",
        "days",
        "km^2/s^2",
        "km^3/s^2",
        "kg",
        "K",
    }
)

Quantity

Bases: BaseModel

Pydantic model for a scalar {value, unit} payload.

Used as the field type in every tool's pydantic output schema where the field carries a scalar physical quantity. The JSON-schema export is what the unit-discipline meta-test recognises as "correctly wrapped".

QuantityVector

Bases: BaseModel

Pydantic model for a vector {value: [...], unit} payload.

The JSON-schema export uses type: array, items: {type: number} for the value field — the unit-discipline meta-test treats this as the canonical vector-quantity shape.

quantity

quantity(value: float | int, unit: str) -> dict[str, Any]

Wrap a scalar numeric value as a {value, unit} dict.

quantity_vector

quantity_vector(
    values: Sequence[float | int], unit: str
) -> dict[str, Any]

Wrap a sequence of numeric values as a {value: [...], unit} dict.

The shape uses the same value key as :func:quantity; tools choose scalar or vector per output field via their pydantic schema.

find_unit_discipline_violations

find_unit_discipline_violations(
    schema: dict[str, Any], *, schema_name: str
) -> list[_UnitDisciplineViolation]

Return every bare-numeric-field violation in schema.

The schema is a JSON-Schema dict (e.g. the output of MyModel.model_json_schema()). The check is recursive across nested objects, arrays, unions, and $defs-style references.

An empty list means the schema satisfies the unit-discipline rule: every numeric field is wrapped in a :class:Quantity or :class:QuantityVector.

is_finite_number

is_finite_number(value: object) -> bool

Return whether value is a real number with no NaN / inf.

Schemas

astrodynamics_mcp.schemas.base

Shared pydantic base models for every tool's input and output schema.

This module is the single source of truth for the JSON-schema shape the MCP SDK exposes to LLM consumers — every tool composes its input/output schema from these primitives so naming, units, and frame conventions stay consistent across the surface.

Every model carries rich :class:~pydantic.Field descriptions and example values; those are what the LLM reads when deciding how to call the tool.

Epoch module-attribute

Epoch = Annotated[
    str,
    BeforeValidator(_validate_epoch),
    Field(
        description="ISO 8601 UTC timestamp with a mandatory time component. Examples: '2026-05-23T12:00:00Z', '2026-05-23T12:00:00.500+00:00'. A bare date like '2026-05-23' is rejected.",
        examples=[
            "2026-05-23T12:00:00Z",
            "2026-01-01T00:00:00.500Z",
        ],
    ),
]

Body module-attribute

Body = Annotated[
    str,
    Field(
        description="Celestial body or satellite name. Accepts SPICE-style identifiers (e.g. '399' for Earth, '301' for Moon), common English names ('earth', 'sun', 'mars'), and well-known satellite names ('hubble', 'iss'). Resolution happens inside the tool that consumes this — schema validation here is intentionally permissive.",
        examples=["earth", "sun", "mars", "hubble", "399"],
    ),
]

NamedStationName module-attribute

NamedStationName = Literal[
    "madrid",
    "goldstone",
    "canberra",
    "svalbard",
    "wallops",
    "esrange",
    "gsfc",
    "jpl",
]

Observer module-attribute

Observer = Annotated[
    NamedStation | ObserverCoordinates,
    Field(
        description='Observer location. Either a named station (`{"name": "madrid"}`) or explicit coordinates (`{"lat": {...}, "lon": {...}, "alt": {...}}`).'
    ),
]

Tle module-attribute

Tle = Annotated[
    TleLines | TleOmm,
    Field(
        description="A TLE in one of two shapes: two raw 69-character lines (`{line1, line2}`) or a parsed OMM JSON object (`{omm: {...}}`)."
    ),
]

TimeScale

Bases: str, Enum

Time-scale identifiers used across the time/frame/access tool surfaces.

Inherits from str so pydantic round-trips the enum as its string value in JSON ("UTC" rather than "TimeScale.UTC").

Frame

Bases: str, Enum

Reference frames used for state vectors and frame conversions.

The v0.1 set covers the inertial, Earth-rotating, and IAU body-fixed frames the wrapped upstreams (sgp4, astropy.coordinates, skyfield) all speak. Adding a new frame here means adding the corresponding transform path to the frame_transform tool.

NamedStation

Bases: BaseModel

An observer identified by a name from the v0.1 station registry.

ObserverCoordinates

Bases: BaseModel

An observer specified by explicit geodetic coordinates.

TleLines

Bases: BaseModel

Two-line TLE as raw strings.

Both lines must be exactly 69 characters (the canonical TLE width). Sub-69-character lines are the most common LLM mistake — they almost always come from a chat client stripping trailing whitespace.

TleOmm

Bases: BaseModel

Parsed OMM (Orbit Mean Elements Message) JSON.

Schema is intentionally loose at v0.1: we accept whatever the upstream OMM source (CelesTrak) emitted. Downstream tools that need specific fields raise typed errors when those fields are missing.

StateVector

Bases: BaseModel

Cartesian state in a named frame at a named epoch.

The position and velocity are :class:QuantityVector instances so the unit (km vs m, km/s vs m/s) is on the wire — never implicit.

Interval

Bases: BaseModel

A time interval bounded by two epochs.

start and end must be UTC ISO 8601; duration_s carries the unit explicitly. The end > start ordering check uses lexicographic string comparison — correct only when both epochs use the same timezone designator. The consuming tool (e.g. access_windows) is responsible for parsing across mixed offsets.

KeplerianElements

Bases: BaseModel

Classical Keplerian orbital elements (a, e, i, RAAN, argp, nu).

Shared across any tool that emits an orbit's elements — Lambert solve (transfer arc), porkchop (transfer-elements grid), bplane (hyperbolic approach orbit). True anomaly nu is at the reference epoch (e.g. the start of the Lambert transfer for lambert_solve).

Semi-major axis a is negative for hyperbolic orbits and undefined for parabolic ones — callers that need to handle the parabolic edge should branch on eccentricity rather than a.

Cache

astrodynamics_mcp.cache

XDG-aware on-disk cache for upstream-data responses.

One JSON file per (source, key) entry under a platformdirs-resolved cache root (~/.cache/astrodynamics-mcp/ on Linux, %LOCALAPPDATA%\astrodynamics-mcp\Cache\ on Windows).

Multi-process safety

The cache supports parallel astrodynamics-mcp stdio / ... http processes reading and writing the same XDG dir at the same time. Two invariants make that work:

  1. Atomic-rename writes. Every write goes to a tempfile in the same directory as its destination, then os.replace swaps it into place. On POSIX rename(2) is atomic; on Windows os.replace is atomic via MoveFileExW(MOVEFILE_REPLACE_EXISTING) since Python 3.3. A reader will see either the prior file or the new one, never a torn half-write. The same-directory invariant is non-negotiable — a tempfile in /tmp and a destination in /home/user/.cache/... would make the rename a cross-filesystem move, which is not atomic.

  2. No locking. Two writers racing on the same key both create their own tempfile and both os.replace; whichever rename lands second wins, neither write is torn, and no reader sees an intermediate state. We deliberately do not use a lockfile — a crashed writer leaving a stale lock would block future writes indefinitely; the rename pattern is self-healing instead.

Disabled mode

Setting ASTRODYNAMICS_MCP_CACHE_DIR="" (empty string) disables the cache: get / get_stale always return None and put is a no-op. Useful in tests and CI cells that want a pristine cache miss.

DEFAULT_TTLS module-attribute

DEFAULT_TTLS: Mapping[str, float] = {
    "celestrak": 6 * 60 * 60,
    "iers": 24 * 60 * 60,
    "horizons": 7 * 24 * 60 * 60,
}

Cache

On-disk cache for upstream-data responses.

See module docstring for the multi-process-safety story.

directory property
directory: Path | None

Resolved cache directory, or None when caching is disabled.

get
get(
    source: str, key: str, *, ttl_s: float
) -> CacheHit | None

Return the cached value if it exists and is younger than ttl_s.

get_stale
get_stale(source: str, key: str) -> CacheHit | None

Return the cached value regardless of age. None if missing.

put
put(source: str, key: str, value: Any) -> None

Write value to the cache atomically. No-op when disabled.

CacheHit dataclass

The result of a successful cache lookup.

default_cache

default_cache() -> Cache

Return the module-level lazy-initialised cache singleton.

Data adapters call this so they share a single cache instance per process. Tests should construct their own :class:Cache with a tmp_path directory and pass it into the code under test explicitly — do not rely on the singleton in tests.