Skip to content

Recipes

Short worked examples covering the canonical workflows. Each recipe is written as the natural-language exchange you'd have with an MCP-capable chat client; the tool calls the LLM makes are shown inline.

Several recipes have a fully worked, reproducible counterpart under examples/ — see the Worked example link at the end of each section.

Fetch a TLE and propagate it

The most common single-satellite question — current TLE for a named object, propagate it to a few epochs.

You: Fetch the latest TLE for the ISS and propagate it to 2026-05-23T12:00:00Z, 14:00, and 16:00.

The model calls tle_lookup with query="25544" (the ISS NORAD ID — or the name "ISS (ZARYA)"). The CelesTrak adapter returns the current TLE plus the parsed OMM JSON; the response is cached for six hours under the XDG cache directory so a repeat call within that window is free.

It then calls sgp4_propagate with the fetched {line1, line2} and the three epochs. The default frame="TEME" (SGP4's native output frame) returns three StateVectors, each with explicit km and km/s units, the frame string, and the epoch they're valid at. Pass frame="ICRF" or frame="GCRS" to receive the same trajectory in an inertial Earth-centred frame; the tool transforms via astropy with the correct per-epoch obstime. See tle_lookup and sgp4_propagate for the full schema.

A TLE for a fresh launch (Space-Track)

You: Get me the latest TLE for NORAD 62000 — it's a recent launch and I don't see it on CelesTrak.

CelesTrak's general-perturbations feed trails the catalogue for very recent launches. The model retries tle_lookup against Space-Track, which carries the deeper, more current GP records:

// tools/call → tle_lookup
{ "query": "62000", "source": "space-track" }

Space-Track requires a per-user account. If no credential is configured, the call returns before any network request with the typed error credential_required.spacetrack, listing the fields it needs. The LLM relays that you must set ASTRODYNAMICS_MCP_SPACETRACK_USERNAME and ASTRODYNAMICS_MCP_SPACETRACK_PASSWORD (stdio) or pass the credential in the initialize _meta block (HTTP) — see Credentials. Once the credential is present, the same call returns the TLE plus parsed OMM JSON, cached on disk for six hours so a repeat lookup within that window costs no request against Space-Track's per-account rate limit.

A numeric query becomes a NORAD_CAT_ID lookup and anything else an OBJECT_NAME substring search; Space-Track has no CelesTrak-style group keywords. See Data sources for the auth, caching, and rate-limit story.

Ground-station passes for a named observer

You: Show me Hubble passes above 10° from Madrid over the next seven days.

The model calls tle_lookup with query="HUBBLE" (or NORAD ID 20580). It then calls access_windows with:

  • observer={"name": "madrid"} — the named-station registry covers Madrid, Goldstone, Canberra, Svalbard, Wallops, Esrange, GSFC, and JPL. Pass observer={"lat": {...}, "lon": {...}, "alt": {...}} for anything else.
  • The fetched TLE.
  • start / end epochs (UTC ISO 8601 with a mandatory time component — bare dates are rejected).
  • min_elevation_deg=10 (degrees, not radians).

The tool returns a list of AccessWindows — each with AOS, LOS, peak-elevation epoch and angle, range at AOS / peak / LOS, and pass duration. Partial passes that straddle a window boundary are omitted; only complete (AOS, peak, LOS) triples are emitted.

Filter on range at peak elevation by passing min_range_km / max_range_km. See access_windows for the full input list.

Worked example: Hubble passes from Madrid — full transcript plus a reproducible script that drives the tle_lookupaccess_windows chain against an in-process MCP server.

A simple porkchop

You: Run a porkchop for Earth → Mars departure 2026-09 through 2026-12, arrival window 2027-04 through 2027-10.

The model calls porkchop with:

  • departure_body="earth", arrival_body="mars".
  • depart_window=["2026-09-01T00:00:00Z", "2026-12-31T00:00:00Z"].
  • arrive_window=["2027-04-01T00:00:00Z", "2027-10-31T00:00:00Z"].
  • Default samples_per_axis and mu="sun".

The tool fetches both bodies' ephemerides from JPL Horizons (cached for a week by default — planetary ephemerides drift on geological scales), runs the Lambert solves across the (depart × arrive) grid, and by default returns the summary shape: the best cell (lowest total Δv), the top five cells, and an ASCII contour of the C3 grid. Pass output="full" to receive every feasible cell. See Output shaping for why the default trims the grid.

Open the best cell from the summary and feed it straight back into a Lambert solve to recover the depart / arrive velocity vectors at native precision.

Worked example: Mars launch window 2028 — planning-tier transcript driving porkchop over a 2028 window with a synthetic Earth/Mars geometry; the run script asserts the best cell sits inside a plausible Δv envelope.

Hohmann Δv between circular orbits

You: Compute the Hohmann Δv from a 250 km circular LEO to GEO.

The Hohmann transfer is the half-ellipse joining perigee at the departure radius to apogee at the arrival radius. The model calls lambert_solve with tof set to half the transfer ellipse's period and supplies both circular-tangent velocities so the response's dv field carries the two-impulse total directly.

// tools/call → lambert_solve
{
  "r1": [6628.137, 0.0, 0.0],
  "r2": [-42163.999..., 0.073..., 0.0],     // 179.999° offset dodges Lambert's strictly-collinear branch
  "tof": 19035.51,
  "mu": "earth",
  "depart_velocity": [0.0, 7.755, 0.0],
  "arrive_velocity": [-0.000005, -3.075, 0.0]
}

Response carries dv ≈ 3.91 km/s plus the transfer arc's semi-major axis (a = 24396 km) and eccentricity (e ≈ 0.73) — the textbook Hohmann values.

Worked example: Hohmann LEO → GEO — full transcript with the geometric setup and the collinear-degeneracy workaround explained, plus a reproducible script asserting the Δv lands within ± 0.01 km/s of the textbook ≈ 3.912 km/s.

Time-scale conversions for log analysis

You: Convert these telemetry timestamps from GPS to UTC and TDB: 2026-05-23T12:00:00Z, 2026-05-23T12:00:01Z.

The model calls time_convert twice (or in a single multi-target call if the prompt permits), once for each target scale. The tool wraps astropy.time so leap-second handling, UT1-UTC corrections (sourced from the cached IERS Bulletin A), and the TDB ↔ TT relativistic offset are all correct.

The same tool handles ISO / JD / MJD / seconds-since-J2000 / Unix-time representations on either side via the in_format / out_format arguments. See time_convert for the full scale and format lists; the time-scale Python type is astrodynamics_mcp.schemas.base.TimeScale.

Plot a satellite ground track

Requires the [viz] extra — see Visualisation for the tool surface and the attachment model.

You: Plot the ISS ground track over one orbit.

A ground track is the sub-satellite latitude/longitude path, read off the Earth-fixed frame. So the model fetches the TLE, then propagates it in ITRS — the rotating frame whose plane the sub-satellite point is taken from, and the cheapest input for the plot tool since no inertial-to-fixed rotation is needed. It samples one ~92-minute revolution (here 31 epochs, three minutes apart) and asks for the full series so the track is smooth:

// tools/call → sgp4_propagate
{
  "tle": { "line1": "1 25544U …", "line2": "2 25544  51.6400 …" },
  "epochs": ["2024-01-01T00:00:00Z", "…31 epochs…", "2024-01-01T01:30:00Z"],
  "frame": "ITRS",
  "output": "full"
}

It feeds that states series straight into the plot tool:

// tools/call → plot_ground_track
{ "states": [ /* the 31 ITRS states */ ] }

plot_ground_track returns three things at once: a PNG ImageContent (the rendered track), a leading ASCII line, and a structured summary — revolutions, and the latitude / longitude extent as {value, unit} quantities. For this orbit the latitude extent comes back ≈ ±51.6°, exactly the orbit's inclination — a circular orbit's track reaches its inclination at its highest and lowest points. An image-capable client shows the PNG; a text-only client still reads the extent from the summary, because the image is additive, never the only answer.

Overlay ground stations by passing stations; they are drawn as markers on the same graticule.

Worked example: ISS ground track — full transcript plus a reproducible script that drives the sgp4_propagateplot_ground_track chain and asserts the PNG and summary come back together.

Export a trajectory as CZML

Requires the [viz] extra — see Visualisation.

You: Export one ISS orbit as CZML so I can load it into Cesium.

CZML animates an object's position over time in an inertial frame, so the model propagates in TEME — sgp4's native inertial frame, which the CZML backend renders with no transform — and hands the state series to czml_trajectory. One revolution sampled every six minutes (16 epochs) keeps the path smooth without bloating the document:

// tools/call → sgp4_propagate
{
  "tle": { "line1": "1 25544U …", "line2": "2 25544  51.6400 …" },
  "epochs": ["2024-01-01T00:00:00Z", "…16 epochs…", "2024-01-01T01:30:00Z"],
  "frame": "TEME",
  "output": "full"
}

The {r, v, frame, epoch} series is exactly what the export tool consumes (velocity is optional but carried here):

// tools/call → czml_trajectory
{ "trajectory": [ /* the 16 TEME states */ ], "style": "default" }

czml_trajectory returns the CZML as an EmbeddedResource — an application/json document keyed by a stable uri (czml://trajectory/satellite) — alongside an ASCII line and a structured summary carrying time_span and the packet / object / contact counts. The document is a Cesium stream: a preamble with a clock spanning the window, plus one satellite object whose time-tagged positions Cesium interpolates into an animated orbit. A client saves resource.text to a .czml file or streams it to a Cesium viewer; no chat client renders the globe inline (astrodynamics-mcp ships no viewer). Annotate ground-station contacts by passing intervals — each becomes a line of sight shown only during its access window.

Worked example: CZML export for Cesium — full transcript plus a reproducible script that drives the sgp4_propagateczml_trajectory chain and asserts a non-empty CZML document comes back.

Query a body's state from SPICE kernels

Requires the [spice] extra — see SPICE integration for the kernel model and the full tool surface.

You: Use SPICE to get the Moon's geocentric state on 2026-01-01T00:00:00Z.

An SPK state query needs two kernels furnished first — a leap-second kernel (to resolve the UTC epoch) and a planetary ephemeris SPK (the states themselves). The model furnishes both from the NAIF allowlist with spice_load_kernel:

// tools/call → spice_load_kernel
{ "source": "https://naif.jpl.nasa.gov/pub/naif/generic_kernels/lsk/naif0012.tls" }
// tools/call → spice_load_kernel
{ "source": "https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/de440s.bsp" }

The pool is additive and persists across calls, so both stay loaded together; a repeat URL load within the cache TTL skips the network (from_cache=true). Then it queries the state — the Moon relative to the Earth, in the default J2000 frame:

// tools/call → spice_state
{
  "target": "MOON",
  "observer": "EARTH",
  "epochs": ["2026-01-01T00:00:00Z"]
}

The response carries one state per requested epoch — position (km) and velocity (km/s) as {value, unit} quantities in the requested frame, each epoch self-describing. aberration defaults to NONE (the geometric state), so light_time comes back null; pass LT or LT+S for a light-time / stellar-aberration-corrected state and the one-way light time is populated. A missing kernel returns a typed error rather than a silent empty state — confirm the pool with spice_list_kernels if a query reports a kernel gap.

Reach for the spice_* tools when an operation needs furnished kernels — an SPK state, a non-Earth body-fixed frame (spice_frame_transform), body constants (spice_body_parameters), or ET / SCLK time (spice_time_convert). For plain UTC / TAI / TT and Earth-centred frames the kernel-free time_convert and frame_transform need no kernels at all.

Worked example: SPICE Mars state — full transcript furnishing an LSK + planetary SPK and cross-checking spice_state against a Horizons-backed porkchop for Mars.

Run a GMAT mission from a skeleton

Requires the [gmat] extra — see GMAT integration for install and the full tool surface.

You: Run a Hohmann transfer from a 7000 km circular LEO to GEO and give me the total Δv.

The client lists the GMAT skeleton resources, reads gmat-skeleton://hohmann-transfer, and edits it for the requested geometry. Before spending a full mission run, it parses the script with gmat_validate_script:

// tools/call → gmat_validate_script
{ "script": "Create Spacecraft LEOSat; ... (edited skeleton text)" }

GMAT returns the declared resources and any parse errors. The model fixes a mistyped field name GMAT flagged, re-validates clean, then runs it:

// tools/call → gmat_run_mission
{
  "script": "Create Spacecraft LEOSat; ... ",
  "overrides": { "LEOSat.SMA": 7000 }
}

The response carries a run_id, the mission summary, and the small report inline; the model reads the two-impulse Δv from the report rows. Ephemerides and oversized reports come back as pointers — pull the full bytes with gmat_read_run_artefact(run_id="…", name="EphemerisFile1"). To explore how the Δv responds across a range of burn magnitudes instead of a single run, reach for gmat_sweep; see GMAT integration for the sweep and validate-then-run patterns in full.