Skip to main content
Glama

WeatherOps MCP

An MCP server and CLI for field-ops, event and construction go/no-go calls. Give it a site (address or lat/lng), a work window and a threshold profile; it pulls the Google Weather API hourly forecast (or the keyless US National Weather Service grid forecast) and returns every hour as GO, MARGINAL or NO-GO, with the exact rule that tripped and the value that tripped it.

Decision support only, not a safety authority. Forecasts can be wrong, and the four bundled profiles are illustrative examples, not safety standards. The responsible person on site, equipment manufacturer limits and applicable regulations decide whether work goes ahead.

CI License

The problem

Agents that plan crane lifts, concrete pours, roof work or outdoor events are asked "can we work tomorrow 07:00-17:00?" and answer from a weather summary. The useful answer is per hour, against the crew's own limits, with a reason a supervisor can check: "14:00 NO-GO, gusts 38 km/h break gust < 35". WeatherOps does the geocoding, paging, unit normalization and threshold evaluation in one read-only tool call.

Related MCP server: weather

Quickstart

bun install --frozen-lockfile
bun run demo          # keyless: synthetic crane-lift window, prints the hour table below
bun run weatherops profiles
bun run weatherops go-no-go --lat 35.7796 --lng -78.6382 --profile crane-lift --start now --end +10h --source nws   # live, keyless (US)
cp .env.example .env  # add GOOGLE_MAPS_API_KEY for Google Weather + Geocoding
bun run weatherops go-no-go --address "..." --profile concrete-pour --start 2026-11-03T07:00:00-05:00 --end 2026-11-03T15:00:00-05:00 --fail-on no-go

Demo output (reports/example-crane-lift.md, synthetic data), first rows:

Hour

Decision

Gust km/h

Temp C

Thunder %

Why

10:00

GO

22

10

5

11:00

MARGINAL

28

11

10

thunderstorm_probability_pct 10% at 11:00 is within 10% of the 20% limit

14:00

NO-GO

38

13

35

wind_gust_kmh 38 km/h at 14:00 breaks gust: wind_gust_kmh < 35 margin 5 (+ thunder, storm-condition)

MCP server

{
  "mcpServers": {
    "weatherops": {
      "command": "bun",
      "args": ["/abs/path/weatherops-mcp/src/cli.ts", "serve"],
      "env": { "GOOGLE_MAPS_API_KEY": "" }
    }
  }
}

Add "--fixtures" to args for the keyless synthetic demo. Five read-only tools:

Tool

Input

Upstream calls

go_no_go

address or lat+lng; start, end (RFC 3339 with offset, now, +Nh; window up to 72 h); profile and/or rules[]; source auto (default) / google / nws; format

1 geocode (addresses only) + 1 per 24 forecast hours (Google), or 2 (NWS)

outlook

location, days 1-10, profile/rules

1 forecast/days call; one decision per 12 h day part

current_conditions

location, optional profile/rules

1 currentConditions call; horizon rules are listed as not checked

geocode_site

address

1 Geocoding v4 call

list_profiles

none

none

Results come back as compact JSON plus Markdown (format: json|markdown|both). Every result carries disclaimer, the profile's own disclaimer, source.used / source.fallback, and the attribution lines Google requires.

Threshold profiles

A rule states what must hold for GO, e.g. wind_gust_kmh < 35:

  • violated: NO-GO (the rule "tripped"; the result names the rule, the value and the hour it came from)

  • holds, but within margin of the limit: MARGINAL

  • the source has no value for the metric: on_missing (marginal by default, or no-go / ignore)

  • horizon_hours: N (inline over Nh) checks this hour and the next N-1, using the worst value (or the total with aggregate: sum). Read each hour as "start the job at this hour".

An hour's decision is the worst of its rules; the window's decision is the worst hour. The summary lists GO blocks, the first NO-GO hour and per-rule counts.

Bundled example profiles in profiles/ (each carries an "EXAMPLE ONLY, not a safety standard" disclaimer):

Profile

Rules

crane-lift

wind_gust_kmh < 35 margin 5; thunderstorm_probability_pct <= 20 margin 10; condition not_in THUNDERSTORM, THUNDERSHOWER, ... HAIL_SHOWERS (Google only); temperature_c > 2 margin 2

concrete-pour

precip_mm <= 0 over 6h (no precipitation in the 6 h after the start); precip_probability_pct < 40 margin 15 over 6h; thunderstorm_probability_pct <= 20; temperature_c >= 5 margin 2 over 12h

outdoor-event

thunderstorm_probability_pct < 20 margin 10; storm conditions; wind_gust_kmh < 50 margin 10; precip_probability_pct < 60 margin 20; feels_like_c < 32 margin 3

roofing

wind_gust_kmh < 40 margin 8; precip_mm <= 0 over 2h; ice_thickness_mm <= 0; temperature_c > 4 margin 2; thunderstorm_probability_pct <= 20

Inline rules (CLI --rule, MCP rules[]) use METRIC OP VALUE [margin N] [over Nh] [sum] [missing marginal|no-go|ignore] [id NAME]; a rule with a profile rule's id replaces it. Metrics: temperature_c, feels_like_c, wind_speed_kmh, wind_gust_kmh, precip_probability_pct, precip_mm, thunderstorm_probability_pct, cloud_cover_pct, relative_humidity_pct, uv_index, visibility_km, ice_thickness_mm (numeric: < <= > >=), condition, precip_type (in / not_in). The CLI also takes --profile-file my.yaml; the MCP server only accepts bundled profile names.

Sources (checked against the references on 2026-10-21)

Source

Endpoint and parameters used

Fields read

Reference

Google Weather hourly

GET https://weather.googleapis.com/v1/forecast/hours:lookup?location.latitude=&location.longitude=&unitsSystem=METRIC&hours=&pageSize=24&pageToken= (hours 1-240, pageSize 1-24)

forecastHours[].interval, temperature, feelsLikeTemperature, wind.speed/gust, precipitation.probability.{percent,type}, precipitation.qpf/snowQpf, thunderstormProbability, weatherCondition.type, cloudCover, relativeHumidity, uvIndex, visibility, iceThickness, isDaytime; timeZone.id; nextPageToken

forecast.hours.lookup, hourly guide

Google Weather daily

GET .../v1/forecast/days:lookup?...&days=&pageSize=10 (days 1-10)

forecastDays[].daytimeForecast / nighttimeForecast (interval, wind, precipitation, thunderstormProbability, weatherCondition, iceThickness), maxTemperature, minTemperature, displayDate

forecast.days.lookup

Google Weather current

GET .../v1/currentConditions:lookup?...

currentTime, timeZone, same condition fields as an hour

currentConditions.lookup

Google Geocoding v4

GET https://geocode.googleapis.com/v4/geocode/address?addressQuery= with X-Goog-FieldMask: results.placeId,results.formattedAddress,results.location,results.granularity

results[0].location.{latitude,longitude}, formattedAddress, placeId, granularity

geocodeAddress, GeocodeResult

NWS (keyless, US)

GET https://api.weather.gov/points/{lat},{lng} then the forecastGridData URL GET /gridpoints/{wfo}/{x},{y} (raw grid)

layers temperature, apparentTemperature, windSpeed, windGust, probabilityOfPrecipitation, probabilityOfThunder, quantitativePrecipitation, skyCover, relativeHumidity, visibility, iceAccumulation (uom + values[{validTime: "START/ISO8601-duration", value}]), timeZone from points

NWS API docs, OpenAPI

The Google key is sent only in the X-Goog-Api-Key header. Values are normalized to metric from the response's own unit fields (FAHRENHEIT, MILES_PER_HOUR, INCHES, WMO unit codes). source: auto uses Google when a key is set; on a missing key or any Google API error (for example 403 SERVICE_DISABLED) it falls back to NWS and says so in source.fallback. Outside NWS coverage /points answers 404, reported as NOT_COVERED.

Why the raw NWS grid instead of /gridpoints/{wfo}/{x},{y}/forecast/hourly: a live hourly response (Raleigh, 2026-10-21) had temperature, a text windSpeed ("6 mph"), probabilityOfPrecipitation, dewpoint and relativeHumidity per period, but no gust, thunder probability or precipitation amount. The raw grid has all three.

Measured

Produced by bun run report (scripts/report.ts): every row is a real call through the MCP server (in-memory client, real tools, service and HTTP client) with Google and NWS answered by the synthetic fixtures. No network, no keys. Source: reports/weatherops-mcp.md and .json. Run on 2026-10-21 (darwin-x64, Bun 1.4.3). "Upstream calls" includes the geocode call and any failed Google call before a fallback.

Scenario

Tool

Profile

Source used

Hours

GO

MARGINAL

NO-GO

First NO-GO (rule, value)

Longest GO block

Upstream calls

harbor-yard crane lift 07-17 EST (Google)

go_no_go

crane-lift

google

10

4

3

3

14:00 EST gust wind_gust_kmh=38

07:00 EST to 11:00 (4 h)

2

harbor-yard crane lift 07-17 EST, forced NWS grid

go_no_go

crane-lift

nws

10

4

3

3

14:00 EST gust wind_gust_kmh=38.9

07:00 EST to 11:00 (4 h)

3

harbor-yard roofing next day 07-17 EST (Google, 2 pages)

go_no_go

roofing

google

10

5

1

4

07:00 EST materials-cold temperature_c=1

12:00 EST to 17:00 (5 h)

3

river-lot concrete pour starts 07-15 MST (Google 403 -> NWS)

go_no_go

concrete-pour

nws (fallback: SERVICE_DISABLED)

8

3

2

3

12:00 MST no-rain-6h precip_mm=0.5

09:00 MST to 12:00 (3 h)

3

coast-plaza outdoor event 08-22 GMT (Google; outside NWS)

go_no_go

outdoor-event

google

14

11

2

1

19:00 GMT thunder thunderstorm_probability_pct=25

08:00 GMT to 18:00 (10 h)

1

harbor-yard inline rules (gust < 25 margin 5; rain < 2 mm over 3 h sum)

go_no_go

inline

google

10

3

1

6

11:00 EST wind-gust-kmh-1 wind_gust_kmh=28

07:00 EST to 10:00 (3 h)

2

river-lot source=google only (403 SERVICE_DISABLED)

go_no_go

roofing

google

error: upstream_403 SERVICE_DISABLED

-

-

-

-

-

-

coast-plaza source=nws only (outside coverage)

go_no_go

outdoor-event

nws

error: upstream_404 NOT_COVERED

-

-

-

-

-

-

harbor-yard 10-day outlook (5 days x day/night)

outlook

crane-lift

google

10 parts

1

2

7

-

-

2

harbor-yard current conditions vs crane-lift

current_conditions

crane-lift

google

now

1

0

0

-

-

2

Google vs NWS on the same synthetic weather (harbor-yard crane-lift 07-17 EST): 10 of 10 hours get the same decision. The storm-condition rule tripped 0 NO-GO hours on NWS (no condition type in the grid, rule set to on_missing: ignore) vs 2 on Google.

Example outputs: crane lift, concrete pour with NWS fallback, outdoor event, 10-day outlook.

Live checks (bun run test:live, 2026-10-21, this machine)

Check

Result

Google Weather forecast/hours, forecast/days, currentConditions (key from dev-test-461117)

HTTP 403 PERMISSION_DENIED, reason SERVICE_DISABLED (Weather API not enabled in that project); the three tests print a skipped: line

Geocoding v4 geocode/address (public landmark address)

HTTP 200, granularity=ROOFTOP, lat/lng inside the expected box; test passes

NWS /points -> /gridpoints -> go_no_go (Raleigh, NC, next 12 h, crane-lift)

13 hours evaluated from grid RAH/75,57; test passes

NWS /points for London

404 -> NOT_COVERED; test passes

source: auto with the key above

fell back to NWS with fallback.reason = SERVICE_DISABLED; test passes

Without a key the Google and Geocoding tests print skipped: no GOOGLE_MAPS_API_KEY; with WEATHEROPS_LIVE=0 (CI) every live test prints skipped: WEATHEROPS_LIVE=0.

Limits

  • Not verified live: Google Weather responses. The Weather API is not enabled in the only project available to this build, so the Google client has been exercised only against synthetic fixtures built from the published reference and example response. Field names and unit enums (CELSIUS/FAHRENHEIT, KILOMETERS_PER_HOUR/MILES_PER_HOUR, MILLIMETERS/INCHES, KILOMETERS/MILES) come from the reference pages linked above; how real responses fill optional fields (for example whether wind.gust or qpf can be absent) is not confirmed.

  • The forecast does not say at what height wind and gusts apply. Crane and work-at-height limits normally refer to wind at height; treat forecast wind as a lower bound.

  • NWS fallback: no condition type, precipitation type or UV; precipitation and ice amounts arrive as 6 h blocks and are spread evenly over each hour; values are resampled from the raw grid, not the NWS hourly text forecast. US only.

  • The outlook tool is coarse: Google day parts report max wind/gust and average thunderstorm probability for 12 h, and only the day's max/min temperature, so a cold night also flags the daytime part for a lower temperature limit.

  • current_conditions is Google only (NWS observations are not wired in).

  • No caching; every call goes upstream. A per-session call budget (WEATHEROPS_MAX_CALLS, default 60) protects billed keys.

  • Work windows are limited to 72 h and must end inside the 240 h hourly forecast (plus any rule horizon).

  • The four profiles are examples written for this lab. They are not derived from any standard, manufacturer chart or regulation.

Development

bun run check is the local gate (and the only CI step): frozen install, skills layout, strict YAML (workflow + profiles), Biome, tsc --noEmit, unit + keyless e2e tests, live tests (skip cleanly), the report, and gitleaks dir. Fixtures are regenerated with bun run fixtures.

Not affiliated

Not affiliated with, endorsed by, or sponsored by Google LLC or the US National Weather Service. Google Maps and Google Maps Platform are trademarks of Google LLC. Google-sourced results print "Source: Includes weather data from Google" and "Includes data from Google Maps" as the Weather API policies require.

Built by SkilledDragon LLC. Questions: hello@skilleddragon.com. Apache-2.0, see LICENSE and NOTICE.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    F
    maintenance
    Weather files (EPW/DDY) for building energy simulation, on-demand. Four tools: search the 17,000-station catalog, analyze any EPW (design conditions, HDD/CDD, DesignDay IDF emit), render 5 chart types (diurnal, wind rose, temp carpet, monthly box plot, comparison), and generate TMY/AMY/CMIP6-morphed scenarios with UHI, extreme events, and wildfire smoke overlays. 3 of 4 tools work without API key.
    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