Skip to main content
Glama

DSN Lifeline

DSN Lifeline watches NASA's Deep Space Network through the public DSN Now feed and queues an alert when one of your favourite spacecraft starts talking to Earth. It runs as a Cloudflare Worker with one SQLite Durable Object, and it exposes the queue as a pull-based MCP server. An AI assistant routine calls pending_alerts on a schedule, tells you about anything new, and calls ack_alerts when done. The server never pushes anything to a phone or any other outside host.

Architecture

flowchart LR
    A[NASA DSN Now feed<br/>eyes.nasa.gov] -->|fetch every 60 s| B[Cloudflare Worker<br/>Durable Object alarm]
    B --> C[(SQLite Durable Object<br/>snapshot, contacts, alerts)]
    C --> D[MCP tools<br/>pending_alerts / ack_alerts]
    D --> E[Assistant routine<br/>polls and messages the user]

src/index.js checks the key and the body size, then hands POST /mcp to one Durable Object named main. src/lifeline.js is a thin shell over the alert queue, the contact tracker, and the saved state. An alarm runs every 60 seconds, fetches at most one feed, and fetches the config at most once per 24 hours. The alarm is re-armed in finally.

A craft counts as talking when an active downlink matches its table spacecraftID on two accepted snapshots in a row. A contact closes after three missed accepted snapshots and reopens if the gap is 14 good polls or fewer. Rejected polls do not count as absence.

Related MCP server: mcp-meteors

Tools

Tool

Arguments

Purpose

dsn_now

watched_only (boolean, required)

Which watched crafts are talking, and on which dish and station

watchlist

none

Current watchlist

watch_craft

code, favourite (boolean)

Add a craft, optionally as a favourite that raises alerts

unwatch_craft

code

Remove a craft

mute_craft

code, hours (integer 0 to 720; 0 clears the mute)

Silence a craft for a while

source_status

none

Feed freshness, coverage, backoff, and row budget

pending_alerts

none

Unacknowledged alerts still inside their 20 minute window

ack_alerts

ids (array of strings)

Mark alerts done after the user was told

test_alert

none

Queue one fixed test alert, not counted as a favourite

pending_alerts returns id, kind, craft, dish, station, and time for each alert. The same array is the text of the tool result, as JSON, so a client can ack without reading the structured block. Kinds are fav, stale, canary, shape, and test. ack_alerts ignores ids that are missing or already finished, and an empty ids list returns INVALID_INPUT. Codes are 2 to 12 characters, lowercase letters and digits. Unknown fields return INVALID_INPUT.

Watched crafts are chosen from a local table in src/crafts.js: Europa Clipper (eurc), Psyche (psyc), IMAP (imap), and SWFO-L1 (swfo).

Setup

Requires Node 22.

  1. Install and test:

    npm ci
    npm test
    npm run typecheck
  2. Local run: copy .dev.vars.example to .dev.vars, set API_KEY to a random string of at least 20 characters, then npm run dev. .dev.vars is gitignored.

  3. Deploy with Wrangler:

    npx wrangler login
    npx wrangler secret put API_KEY
    npx wrangler deploy
  4. Set DEFAULT_TZ (an IANA zone such as America/Toronto, which is also the default) as a plain variable in the Cloudflare dashboard. wrangler.toml sets keep_vars = true so it survives deploys.

  5. Point an MCP client at https://dsn-lifeline.<your-subdomain>.workers.dev/mcp with header Authorization: Bearer <API_KEY> or X-API-Key: <API_KEY>.

GET /health returns {"ok":true,"spec_version":2} and does not call the Durable Object.

Assistant routine

Run this prompt as a routine every 5 minutes in an assistant that supports MCP tools:

Every 5 minutes, call the DSN Lifeline tool pending_alerts.
Read the text of the result. It is a JSON array. Each item has id, kind, craft, dish, station, and time.
If the array is empty, stay quiet.
For each alert, send the user one chat message:
- kind fav, when dish and station are both present: "<craft> is talking at <time> on dish <dish> at <station>."
- kind fav, when dish or station is missing: "<craft> is talking at <time>."
- kind stale: "DSN Lifeline: no fresh snapshot for 6 hours."
- kind shape: "DSN Lifeline: the feed shape looks wrong."
- kind canary: "DSN Lifeline: no spacecraft talking seen in 24h. Check the matcher."
- kind test: "DSN Lifeline: test alert at <time>."
Then call ack_alerts with the ids from that JSON.

Example: asking "Is Europa Clipper talking?" calls dsn_now and yields something like Europa Clipper, dish DSS14, complex Goldstone, talking yes, snapshot age 42 seconds. When a favourite craft talks for the first time in a local day, pending_alerts returns one fav row with the craft, dish, station, and local time.

Safety

The feed is untrusted XML. The server never copies feed names, debug strings, or target names into an alert, a log line, or a tool result.

  • The key is required. API_KEY must be at least 20 characters. A missing, short, or wrong key returns 401 and does not reveal whether the server is configured. That path never touches the Durable Object. A valid key with a timezone the server cannot use returns NOT_CONFIGURED.

  • Chat lines are a fixed template. The routine uses the craft name from the local table, a dish matching DSS plus two digits, a station of Goldstone, Canberra, or Madrid, and a local time. Those lines match ^[A-Za-z0-9 .,:()'-]{1,140}$. When dish or station is missing, the routine omits that part.

  • Outbound hosts are fixed. Only eyes.nasa.gov. There is no feed base URL override and no notification host.

  • Auth runs first. The key check and the 16,384 byte body cap both happen before any Durable Object call.

Design decisions

Free-tier row budget. Durable Object SQLite on the free plan allows 100,000 row writes per day. A quiet day here costs about two writes a minute (the alarm plus one snapshot), around 2,880 rows, so roughly 3k writes a day. At 70,000 rows today the poll interval becomes 120 seconds until UTC midnight. At 80,000 the server latches a budget flag for that UTC day and stops queueing chat alerts. source_status reports the counter. Pending alerts are capped at 100 and the oldest is dropped to make room.

100-parameter batching. Durable Object SQLite limits a statement to 100 bound parameters. ack_alerts updates ids in batches of 100, so a long id list cannot fail on the cap.

Constant-time auth. Both the presented key and the expected key are hashed with SHA-256 and the digests are compared with timingSafeEqual. Hashing first gives equal-length inputs, so neither the length nor the content of the key leaks through timing.

Size caps. Request bodies over 16,384 bytes are rejected, using Content-Length when present and a capped stream read when it is absent. A malformed Content-Length is rejected before any buffering. Feed bodies over 256 KB are rejected.

Pull, not push. A push path needs an outbound notification host, a stored topic or token, and delivery retries. Pull keeps the server's only outbound host as NASA, leaves no push credential to leak, and lets the assistant decide how and when to tell the user. The cost is latency of up to one routine interval. Each alert stays relevant for 20 minutes, so a 5 minute routine has several chances to see it.

Testing

npm test runs Node's built-in test runner. Tests use a fake clock, a fake fetch, and node:sqlite, so they never call NASA. node scripts/write-fixtures.js rewrites the XML fixtures.

Last run on Node 22 (v22.23.3): 121 tests across 20 test files, 121 pass, 0 fail, 0 skipped (120 existing plus the new version test). npm run typecheck (TypeScript strict with checkJs) reports no errors. CI runs both on every push (.github/workflows/ci.yml).

How it was built

The spec was locked before coding. Behaviour was written test first, and each change then went through an independent review loop in which a reviewer who had only the spec and the diff looked for faults, and findings were fixed and re-checked. The 1.0.1 version fix followed the same rule: a failing test asserting that serverInfo.version equals the package.json version was written before the bump.

Limitations

  • Only Psyche (psyc, -255) has been seen on the live feed. The IDs for Europa Clipper (-159), IMAP (-43), and SWFO-L1 (-231) are not live-verified, so those alerts may never fire until the IDs are checked.

  • Step 0 gate result, from one recorded run of gates/step0: 60 of 60 polls succeeded, and Psyche was talking in 24 of the 60. The gate is a throwaway Worker. Do not deploy it, because a run polls NASA.

  • There is no push of any kind. Alerts only reach you if a routine polls the queue.

  • The feed has no documented stability guarantee. The server checks the shape and queues a shape alert when it looks wrong.

  • Tests do not run inside the real Workers runtime. test/workerd-shape.test.js only checks the export shape.

Limits

Watchlist max 12. Favourite alerts: 1 per craft per local day, 6 created per local day, including dropped rows and one retry if that alert expires while the contact is still talking and the cap still has room. System alerts (stale, canary, and shape, counted by the local day they were created): 10 created per local day, including dropped rows, and one retry while the condition still holds. Pending queue: 100. Each alert is relevant for 20 minutes. Rows older than 2 days are deleted once each UTC day. Deleting them does not queue that stale or canary alert again. At local midnight the local day changes, a craft can raise one favourite again on the new local day, and the daily caps reset. An alert queued before local midnight stays until it is acknowledged or its 20 minute window ends. The caps do not reset at UTC midnight, which is only the row-budget day. Muting or unwatching a craft drops its pending favourite alerts. watch_craft changes: 10 per local day. mute_craft and unwatch_craft: 30 calls per local day each. test_alert: 3 per hour. All tools: 60 calls per UTC hour.

Credits

Spacecraft contact data comes from NASA/JPL's DSN Now feed. This project is not affiliated with or endorsed by NASA or JPL. Check their terms of use before running it against the feed, and keep the polling rate modest.

License

MIT. See LICENSE.

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    Enables AI assistants to interact with Yamcs mission control systems through natural language, providing tools and resources for telemetry, commands, links, storage, instances, and alarms.
    34
    4
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    MCP server providing AI agents with access to NASA and space/astronomy data including APOD, Mars rover photos, near-Earth asteroids, exoplanets, Earth imagery, natural events, and space weather.
    11
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to access curated space and spaceflight RSS/Atom feeds via tools to list feeds, read feed items, and fetch any feed by URL.
    27 npm
    MIT