The MCS never fetches passes. Third-party scripts push them, at their own pace: a flight
dynamics system (FDS) pushes its predictions, a ground station provider its bookings.
The MCS exposes an injection contract on its HTTP API, not a polling frequency. This page shows
how to write such a pass feeder, with plain HTTP or with the PassFeeder class of the
Python SDK. See Passes for how the MCS merges and uses them.
Identity and source#
Each feeder has its own service identity and writes only the passes of its source. Feeders are declared in the global configuration of the MCS, identity → source:
passes:
feeders: {fds-feeder: fds, station-feeder: provider}The API takes the identity of the caller from its token (Authorization: Bearer <token>, where
an identity provider is configured), or from X-Stellar-User. A caller that is not a feeder
gets 403 pass::not-a-feeder; a feeder writing another source 403 pass::forbidden-source. See
Identity and Roles.
The pass#
| Field | Type | Notes |
|---|---|---|
id | token | Stable identifier, chosen by the feeder: give a pass the same one each time you refine it |
target | token | A target of the current configuration (else 422 pass::unknown-target) |
station | token | Ground station; a gateway carrying this tag serves the pass |
aos, los | RFC 3339, UTC | AOS before LOS |
max_elevation | angle | "47.3 deg", between 0 and 90° |
uplink | boolean, default true | false: a receive-only pass, which refuses any telecommand |
transceiver | optional | On-board transceiver: sband, xband… |
protocol | optional | ccsds, csp, dvb-s2… |
rates | optional | {uplink: "64 kbps", downlink: "2 Mbps"}: expected bit rates, used by the transfer manager when no measured rate is available |
source | fds or provider | The source of the feeder |
status | predicted, booked or cancelled | A fds pass cannot be booked |
The MCS adds feeder, updated_at and, for a pass merged from both sources, contributions
(the last version from each source).
Two modes#
One pass: upsert#
POST /v1/passes writes one pass, new or refined, and answers the pass as stored (merged with
the other source's), with the gateway checked for it:
curl -X POST http://mcs:8080/v1/passes \
-H 'X-Stellar-User: fds-feeder' -H 'Content-Type: application/json' \
-d '{"id": "gs-a-20261002T1014-sat1", "target": "sat1-fm", "station": "gs-a",
"aos": "2026-10-02T10:14:32Z", "los": "2026-10-02T10:24:05Z",
"max_elevation": "47.3 deg", "source": "fds", "status": "predicted"}'A complete snapshot: replace a window#
POST /v1/passes/replace_window takes every pass of a source for a target over an interval: each
pass is written, and the contribution of the source to every pass of the interval absent from
the snapshot is cancelled. It is the natural mode of a script that exports its whole planning.
{
"source": "fds",
"target": "sat1-fm",
"from": "2026-10-02T00:00:00Z",
"to": "2026-10-03T00:00:00Z",
"passes": [ { "id": "gs-a-20261002T1014-sat1", "…": "…" } ]
}The reply lists the identifiers written and cancelled: {"upserted": […], "cancelled": […]}.
Every pass of the snapshot must be of the target and overlap the interval. A booking that stays
in place keeps the merged pass booked.
Matching and merging#
A submitted pass is matched, in order, with the pass stored under its identifier, with the pass
one of whose contributions has that identifier, then with a pass of the same target and station
whose window overlaps. Otherwise it is new. The merged pass keeps the identifier under which it
was first stored; it takes from the booking the window, the elevation, the status and uplink;
transceiver, protocol and rates come from the booking, else from the prediction. Writes are
conditional and retried on conflict.
The Python PassFeeder#
from stellar_mcs import Pass, PassFeeder, PassRefused
async with PassFeeder("http://mcs:8080", "fds-feeder", source="fds") as feeder:
try:
replaced = await feeder.replace_window("sat1-fm", start, end, [
Pass("gs-a-20261002T1014", "sat1-fm", "gs-a", aos, los, max_elevation=47.3),
])
print(replaced.upserted, replaced.cancelled)
except PassRefused as refused:
print(refused.status, refused.codes)| Member | Role |
|---|---|
PassFeeder(url, identity=None, *, source, token=None, timeout=10.0) | A feeder of source ("fds" or "provider"). With token, it sends Authorization: Bearer; else X-Stellar-User: identity. An async context manager (close() otherwise). |
await upsert(one) | Writes one pass; returns the Pass as stored |
await replace_window(target, start, end, passes) | Sends a snapshot; returns Replaced(upserted, cancelled) |
await passes(target=None, *, all=False) | The passes the MCS holds, not over (every one with all) |
Pass(id, target, station, aos, los, max_elevation, status="predicted", uplink=True, transceiver=None, protocol=None, uplink_rate=None, downlink_rate=None) | A pass; elevation in degrees, rates in bit/s, times as aware datetime. source, feeder, updated_at and contributions are filled by the MCS. |
PassRefused | The MCS refused: status, errors ([{code, message, help}]) and codes |
The example feeder#
examples/python/pass_feeder.py reads the planning an FDS exports as CSV, one pass per line, and
sends it as one snapshot per target over the interval its passes cover:
id,target,station,aos,los,max_elevation,uplink,status
gs-a-20261002T1014-sat1,sat1-fm,gs-a,2026-10-02T10:14:32Z,2026-10-02T10:24:05Z,47.3,true,
gs-b-20261002T1151-sat1,sat1-fm,gs-b,2026-10-02T11:51:10Z,2026-10-02T11:58:40Z,18.6,false,cancelled
gs-a-20261002T1327-sat1,sat1-fm,gs-a,2026-10-02T13:27:02Z,2026-10-02T13:37:55Z,71.0,true,uplink (true by default) and status are optional: a pass of the FDS is predicted, one of
a provider booked, unless the file says cancelled.
cd examples/python
uv run pass_feeder.py planning.csv # identity fds-feeder, source fds
uv run pass_feeder.py planning.csv --watch 60 # pushes again when the file changes
uv run pass_feeder.py bookings.csv --identity station-feeder --source provider
uv run pass_feeder.py --demo sat1-fm # invented passes of the next 24 h
stellar passes --target sat1-fmbookings.csv holds a booking (ksat-7781) that overlaps the first prediction on the same
station: the MCS merges them, and the merged pass takes the window and status of the booking. A
pass removed from the next export of the planning is cancelled, since the planning is sent as a
snapshot of its interval. The script takes --api (default http://127.0.0.1:8080).
The core of the script:
async def push(feeder: PassFeeder, passes: list[Pass]) -> None:
"""One snapshot per target, over the interval its passes cover."""
by_target: dict[str, list[Pass]] = defaultdict(list)
for p in passes:
by_target[p.target].append(p)
for target, planned in sorted(by_target.items()):
start = min(p.aos for p in planned)
end = max(p.los for p in planned)
try:
replaced = await feeder.replace_window(target, start, end, planned)
except PassRefused as refused:
log.error("%s: passes refused: %s", target, refused)
continue
log.info("%s: %d passes written, %d cancelled", target,
len(replaced.upserted), len(replaced.cancelled))From the CLI#
For a one-off import, stellar passes import <file> sends a file in the format of the
specification (passes:), as the feeder named by --as; with a header
replace_window: {source, target, from, to}, it sends a snapshot. See the
CLI Reference.
Errors#
| Status | Code | Meaning |
|---|---|---|
| 401 | auth::invalid-token, auth::no-provider | Token refused, or no identity provider configured |
| 403 | pass::not-a-feeder | The identity is not in passes.feeders |
| 403 | pass::forbidden-source | The feeder writes another source |
| 422 | pass::invalid | Invalid body or pass (window, elevation, rates, a fds pass booked…) |
| 422 | pass::unknown-target | No such target in the current configuration |
| 503 | api::unavailable, api::no-configuration | NATS unavailable, or no configuration published |
See the API reference for the full schemas.