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
¶
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
¶
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
¶
Wrap a scalar numeric value as a {value, unit} dict.
quantity_vector
¶
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
¶
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:
-
Atomic-rename writes. Every write goes to a tempfile in the same directory as its destination, then
os.replaceswaps it into place. On POSIXrename(2)is atomic; on Windowsos.replaceis atomic viaMoveFileExW(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/tmpand a destination in/home/user/.cache/...would make the rename a cross-filesystem move, which is not atomic. -
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.
CacheHit
dataclass
¶
The result of a successful cache lookup.
default_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.