Skip to main content
Glama

WeatherWake MCP

Park a weather condition instead of polling for it. An agent calls register_wake with a site and a rule such as "gust < 30 km/h and no precipitation for 3 h, within the next 48 h". The call returns at once and the session can end. A small local worker checks the forecast on a budgeted cadence. When the forecast shows the window (or the deadline passes), it POSTs a signed webhook so a durable workflow resumes.

Forecast-based trigger, not a safety authority. A wake.met webhook means a forecast showed the window you asked for. Forecasts can be wrong. The receiving workflow and the responsible person decide whether work goes ahead.

CI License

The problem

Agents that wait on weather tend to poll it inside a long chat ("check again in an hour") and keep an expensive session open while the sky decides. The waiting belongs outside the model. Register the condition once, let a cheap process watch the forecast within an API budget, and wake the workflow with a webhook that the receiver can verify.

Related MCP server: Rain Check

How it works

agent --register_wake--> MCP server --writes--> .weatherwake/wakes/<id>.json   (returns in ~2 ms)
                                                     |
                          weatherwake worker (loop or cron) -- forecast: NWS / Google / synthetic
                                                     |   (daily call budget, per-site cache, 30-180 min cadence)
                          condition met or expired --+--> outbox (durable, retries)
                                                     |
                          POST WEATHERWAKE_WEBHOOK_URL   webhook-id / webhook-timestamp / webhook-signature
                                                     |
                          receiver: verify HMAC + timestamp + replay guard --> enqueue resume job (idempotent per wake)
                                                     |
                          resume worker / your workflow engine continues the parked run

A wake is met when the forecast shows at least for_hours consecutive hours where every rule holds. The window must end before expires_at and start within lead_hours of now, and it must be seen on confirmations consecutive checks (spaced at least 60 min apart). A wake expires once no such window can still fit before the deadline. That check makes no upstream call.

Cadence: with no window in the forecast, the worker checks again after the max interval (180 min). With a window further out, it checks again lead_hours before the window starts, clamped to 30-180 min, because forecasts move. While it waits for confirmations, the spacing is 60 min. Wakes on the same site share one cached forecast for 20 min. Every upstream request first takes a unit from a per-UTC-day budget (default 200). Errors that will not fix themselves (outside NWS coverage, a missing key, the API not enabled) back off to the max interval.

Quickstart

bun install --frozen-lockfile
bun run demo                       # keyless: simulated clock + synthetic forecast; real MCP, worker, HTTP, receiver, queue

Live (keyless, US only, via the National Weather Service):

cp .env.example .env
bun run src/cli.ts secret          # paste into .env as WEATHERWAKE_WEBHOOK_SECRET (do not share it)
bun run receiver                   # terminal 1: verifying receiver on 127.0.0.1:8787
bun run worker                     # terminal 2: checks due wakes every 60 s, delivers webhooks
bun run src/cli.ts preview  --lat 35.7796 --lng -78.6382 --rule "wind_gust_kmh < 30" --rule "precip_mm <= 0" --for-hours 3 --source nws
bun run src/cli.ts register --lat 35.7796 --lng -78.6382 --rule "wind_gust_kmh < 30" --rule "precip_mm <= 0" \
  --for-hours 3 --within-hours 48 --resume '{"run":"crane-lift-42","step":"schedule-crew"}' --key crane-lift-42

MCP server

{
  "mcpServers": {
    "weatherwake": {
      "command": "bun",
      "args": ["/abs/path/weatherwake-mcp/src/cli.ts", "serve"],
      "env": { "WEATHERWAKE_DATA_DIR": "/abs/path/state", "WEATHERWAKE_WEBHOOK_URL": "https://your-app.example/hooks/weatherwake" }
    }
  }
}

Put the signing secret (and an optional GOOGLE_MAPS_API_KEY) in the environment of both the server and the worker. Do not put them in the MCP config you share. The worker must use the same WEATHERWAKE_DATA_DIR.

Tool

Input

Upstream calls

register_wake

lat, lng, rules[], for_hours (1-24, default 1), within_hours (1-240, default 48), lead_hours (0-48, default 1), confirmations (1-5, default 1), source (auto / nws / google / synthetic), resume (JSON <= 8 KB, echoed in the webhook), idempotency_key, label

none (returns immediately)

preview_condition

site + condition

1 forecast fetch (or a cache hit)

get_wake / list_wakes

wake_id / optional state

none

cancel_wake

wake_id, reason

none

wake_status

none

none. Reports budget used today, cadence, worker heartbeat (warns if no tick in 10 min), signing configured

The tools take no webhook URL: the target comes from WEATHERWAKE_WEBHOOK_URL, so a prompt cannot redirect wake-ups to another host.

Rule grammar: METRIC OP VALUE [missing ok|fail]. Every rule must hold in an hour. A missing value fails the hour unless the rule says missing ok. The numeric metrics are wind_gust_kmh, wind_speed_kmh, precip_mm, precip_probability_pct, thunderstorm_probability_pct, temperature_c, feels_like_c, cloud_cover_pct, relative_humidity_pct and visibility_km; they take < <= > >=. The metrics condition and precip_type take in / not_in with UPPER_CASE lists and come from Google only. "No precipitation for 3 h" is precip_mm <= 0 with for_hours: 3.

The webhook

Standard Webhooks format. Full field list and receiver contract: docs/WEBHOOK.md.

webhook-id:        msg_...            same id on every retry of one event
webhook-timestamp: 1793768400         unix seconds of this attempt
webhook-signature: v1,<base64 HMAC-SHA256(secret, "{id}.{timestamp}.{raw body}")>
  • The body is canonical JSON (sorted keys, no whitespace). Its type is wake.met or wake.expired. It carries wake_id, webhook_id (equal to the header), window (start, end, the end of the clear run, and hour values), condition, resume, source, attribution and stats.

  • The secret is whsec_ + base64 of 24-64 bytes. Several secrets can be configured for rotation: the first signs, all verify.

  • The receiver (src/receiver.ts, examples/receiver.ts) verifies the raw body in constant time. It rejects timestamps more than 300 s off and remembers accepted ids on disk, so a replay gets 200 duplicate and does nothing. It then enqueues the resume job with idempotency key wake:<wake_id>.

  • Interop: tests/unit/signing.test.ts checks that the reference standardwebhooks npm package (1.1.1) verifies our signatures and that we verify its signatures.

  • Delivery: 2xx means done. 408, 429, 5xx and network errors are retried with the same id (backoff 30 s doubling, 8 attempts). Any other 4xx is permanent. The worker never sends an unsigned webhook.

Durable workflow engines

docs/WORKFLOW_MAPPING.md maps the park / resume steps onto Vercel Workflow (createHook + resumeHook from workflow/api), Trigger.dev (wait.createToken / wait.forToken / wait.completeToken) and Hatchet (ctx.waitForEvent + hatchet.events.push). The mapping was written from each package's published type declarations and docs (workflow 5.2.3, @trigger.dev/sdk 4.7.6, @hatchet-dev/typescript-sdk 1.38.0 on npm, 2026-10-21). None of them is a dependency, and the snippets were not executed here.

Measured

From bun run report (scripts/report.ts → reports/weatherwake-mcp.md, reports/weatherwake-mcp.json) on Bun 1.4.3, darwin/x64. The forecasts are synthetic (fixtures/sequences) and run under a simulated clock. The MCP server, worker, cadence, budget, cache, HTTP delivery, signature checks, replay guard and resume queue are the real code. "15-min polling" is arithmetic for comparison (settle time / 15 min), not a measurement.

Run

Wake

Event

Settled (h)

Window start (h)

Window holds in synthetic truth

Checks

Upstream calls

Cache hits

Budget deferrals

Delivery (attempts)

15-min polling (computed)

storm-clears

storm-clears wake 1

wake.met

18

19

yes

7

7

0

0

delivered (3)

72

never-clears

never-clears wake 1

wake.expired

46

-

-

16

16

0

0

delivered (1)

184

flapping-c1

flapping wake 1

wake.met

9

10

no

4

4

0

0

delivered (1)

36

flapping-c2

flapping wake 1

wake.met

30

30

yes

13

13

0

0

delivered (1)

120

shared-site

gust<30 + dry, 3 h

wake.met

18

19

yes

7

7

0

0

delivered (1)

72

shared-site

gust<40, 2 h

wake.met

16

17

yes

7

1

6

0

delivered (1)

64

shared-site

dry + PoP<30, 6 h

wake.met

17

18

yes

7

1

6

0

delivered (1)

68

budget-cap

never-clears wake 1

wake.expired

46

-

-

17

14

0

3

delivered (1)

184

What the runs show:

  • storm-clears: early forecasts put the clearing 6 h too late. The worker spaces checks 3 h apart and wakes at hour 18 for a window starting at hour 19, after 7 checks. The receiver answered 503 twice first, and the third attempt was accepted with the same webhook-id.

  • flapping: one forecast issuance shows a calm spell that never happens. With confirmations: 1 the wake fires on it, and the window does not hold in the synthetic truth. With confirmations: 2 it fires 21 h later, on a window that holds.

  • shared-site: three wakes on one site made 21 checks with 9 upstream calls.

  • budget-cap: with WEATHERWAKE_MAX_CALLS_PER_DAY=6, 3 checks were deferred to the next UTC day instead of overspending.

  • After delivery in the storm-clears run, the receiver answered the following probes. Replaying the same request got 200 duplicate. A tampered body got 401 bad_signature, and so did a wrong secret. The same request 10 min later got 401 timestamp_too_old. A request with no signature got 401 missing_headers.

  • register_wake through the in-memory MCP client: p50 1.94 ms over 20 calls, no upstream calls (this machine; see the report).

Timeline of the storm-clears run: reports/demo-timeline.md.

Local gate (bun run check, this machine): install, skills layout, strict YAML, Biome, tsc --noEmit, 71 unit + keyless e2e tests, 3 live tests, report, gitleaks. All green. The live tests: NWS preview and a worker tick against api.weather.gov passed. The Google Weather test skipped with 403 SERVICE_DISABLED, because the Weather API is not enabled in the key's project.

Configuration

All optional; see .env.example. Bun loads .env automatically.

Variable

Default

WEATHERWAKE_WEBHOOK_URL

http://127.0.0.1:8787/hooks/weatherwake (https anywhere, http only for localhost)

WEATHERWAKE_WEBHOOK_SECRET

none: the worker refuses to deliver without one

WEATHERWAKE_DATA_DIR

./.weatherwake

WEATHERWAKE_MAX_CALLS_PER_DAY

200 upstream calls per UTC day

WEATHERWAKE_MIN_INTERVAL_MIN / MAX_INTERVAL_MIN / CONFIRM_INTERVAL_MIN / CACHE_TTL_MIN

30 / 180 / 60 / 20

WEATHERWAKE_MAX_ARMED / MAX_DELIVERY_ATTEMPTS

50 / 8

GOOGLE_MAPS_API_KEY

none: auto then uses the keyless NWS

Tests

bun run test        # unit + keyless e2e: simulated scenarios, MCP tools, stdio server + worker processes, CLI
bun run test:live   # NWS (keyless) + Google Weather (skips without a key or on 403 SERVICE_DISABLED); WEATHERWAKE_LIVE=0 skips all
bun run check       # the full local gate (CI runs the same script, keyless)

Limits

  • The forecast is the signal. A wake fires on a forecast window, not on observed weather. The synthetic flapping run shows a false wake with confirmations: 1.

  • Sources. NWS covers the US only; elsewhere you need a Google Maps Platform key with the Weather API enabled. The Google path is unit-tested against synthetic responses with the documented shape, but it was not verified live here: the Weather API is not enabled in the project used for this build (403 SERVICE_DISABLED). The rules condition / precip_type and thunderstorm probability need Google; NWS gives 6 h precipitation blocks, which are spread evenly per hour.

  • Sites are lat/lng only. There is no geocoding.

  • The worker must be running (a loop or cron worker --once). The MCP server alone never checks anything; wake_status warns when no worker has ticked.

  • Single host. The state, outbox, replay guard and queues are files that rely on atomic rename on one filesystem.

  • Delivery retries stop after 8 attempts (about an hour). That is shorter than the Standard Webhooks example schedule. After that the wake shows delivery.status: failed.

  • Not measured here: token or dollar savings from ending the chat session, and whether Vercel Workflow, Trigger.dev or Hatchet bill a parked run. The idea's premise of an "idle 200K session" is not quantified by this repo.

  • Not included: Gemini (named in the idea as optional; not used), adapters that call the workflow engines (the mapping doc only).

Not affiliated

Not affiliated with, endorsed by, or sponsored by Google LLC, the US National Weather Service, Vercel Inc., Trigger.dev, Hatchet, or the Standard Webhooks project. Google Maps Platform is a trademark of Google LLC.

License

Apache-2.0. See LICENSE and NOTICE (code reused from the SkilledDragonCorp labs weatherops-mcp and pausabudget-gate).

Built by SkilledDragon LLC as an educational minilab. Questions: hello@skilleddragon.com.

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    Enables AI agents to create on-chain and web monitors, dead-man switches, cron triggers, and multi-agent coordination with pay-per-use x402 payments.
    19
    24 npm
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables users to check whether an Australian field-service job can be worked at a location on a given day for a specific trade, using live weather forecasts, daylight hours, and public holiday data.
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables sleeping agents to monitor multiple event sources and wake only when durable composite or temporal conditions are met, supporting derived events and targeted wakeups without keeping an agent loop alive.
    133 npm
    1
    Apache 2.0