
Wraps NOAA's Climate Data Online API v2 to search weather stations and pull historical observations going back to 1763. You get seven tools covering the full discovery flow: list datasets (GHCND daily, GSOM monthly, climate normals), find locations by state or zip, search stations by bounding box or data type, and fetch observation records with automatic date range validation. The fetch tool enforces per-dataset limits (one year for daily, ten for monthly) and handles unit conversion since raw GHCND values come as tenths-of-unit integers. Includes resources for dataset metadata and station lookups. Useful when you need programmatic access to temperature, precipitation, wind, or snow data without writing pagination and validation logic yourself.
Search NOAA climate stations and datasets, fetch historical weather observations via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://noaa-climate.caseyjhand.com/mcp
10 tools — 8 over the NOAA Climate Data Online (CDO) API v2, plus two over separate NCEI bulk-CSV corpora: severe-weather event search over the Storm Events Database, and disaster costs over Billion-Dollar Weather and Climate Disasters:
| Tool | Description |
|---|---|
noaa_climate_list_datasets | List available CDO datasets with IDs, names, and temporal coverage |
noaa_climate_list_data_categories | List data category groups (Temperature, Precipitation, Wind, etc.) |
noaa_climate_list_data_types | List specific measurement labels (TMAX, TMIN, PRCP, SNOW, etc.) by dataset or category |
noaa_climate_list_location_categories | List the 12 location categories that scope location search |
noaa_climate_find_locations | Search geographic locations by category (states, cities, counties, zip codes, climate regions), with an optional name filter |
noaa_climate_find_stations | Search weather stations by location, bounding box, dataset, and data type |
noaa_climate_get_station | Fetch full metadata for a single station by ID |
noaa_climate_fetch_data | Fetch historical observation records for a dataset and date range |
noaa_climate_search_storm_events | Search the NCEI Storm Events Database for one year — tornadoes, hail, floods, hurricanes, with damage, casualties, and narratives |
noaa_climate_get_billion_dollar_disasters | Query NOAA's Billion-Dollar Weather and Climate Disasters — CPI-adjusted costs and deaths per disaster, or per-year totals by disaster class |
noaa_climate_list_datasetsList all available NOAA CDO datasets — approximately 11 in total.
noaa_climate_fetch_datanoaa_climate_list_data_categoriesList data category groups that organize related measurement types.
noaa_climate_list_data_types to narrow by measurement domainnoaa_climate_list_data_typesList specific measurement labels for a dataset or category.
GHCND) or category (e.g., TEMP) to narrow resultsTMAX (max temperature), TMIN (min temperature), PRCP (precipitation), SNOW (snowfall), SNWD (snow depth), AWND (average wind speed)noaa_climate_list_location_categoriesList the location categories that scope noaa_climate_find_locations — 12 in total.
CITY, ST, CNTY, CNTRY, ZIP, US_TERR, CLIM_REG, CLIM_DIV, HYD_ACC, HYD_CAT, HYD_REG, HYD_SUB) and their nameslocationCategoryId to passnoaa_climate_find_locationsSearch geographic locations by category.
ST (US states, 51), CNTY (counties), CITY (cities), CNTRY (countries), ZIP (zip codes), US_TERR (US territories), CLIM_REG (NOAA climate regions), CLIM_DIV (climate divisions), hydrological categories — noaa_climate_list_location_categories returns the authoritative setlocationCategoryId=ST to list all states in one callnameContains gives the name search the CDO API lacks: the server enumerates the requested category and matches the substring case-insensitively, so locationCategoryId=CITY with nameContains=seattle resolves a city in one call. It is a size rule, not a category list — the category must hold at most 4,000 locations, which is every category but ZIP (30,415), and a datasetId or datacategoryId filter can bring a larger one back under the limit. Past it, page alphabetically with sortField=nameFIPS:37, CITY:US530018, ZIP:98101) used in station search and data queriesnoaa_climate_find_stationsSearch weather observation stations.
datasetId and date range to ensure compatibilityGHCND:USW00024233, COOP:010008noaa_climate_fetch_datanoaa_climate_get_stationFetch full metadata for a single weather station by ID.
noaa://stations/{stationId} resource as a direct lookupnoaa_climate_fetch_dataFetch historical observation records from a NOAA CDO dataset.
datasetId, startDate, and endDate; optionally scoped by station, location, and data typeunits=metric (SI) or units=standard (Fahrenheit/inches). Without it, GHCND values are raw tenths-of-unit integers (TMAX=256 = 25.6°C, PRCP=12 = 1.2mm); GSOM/GSOY are already scaledstartDate=2010-01-01 and endDate=2010-12-31 — that is the API proxy year regardless of which 30-year period is described{ date, datatype, station, value, attributes } with pagination metadatanoaa_climate_search_storm_eventsSearch the NCEI Storm Events Database — a different NOAA corpus from the CDO tools above.
NOAA_CDO_TOKEN is irrelevant to this toolyear is required. NCEI publishes one gzip file per year (~12 MB, ~70k events for a recent year), so an unscoped search would download every year back to 1950state takes the full name NCEI writes — "FLORIDA", not "FL". eventType is matched case-insensitively against the exact NWS label ("Tornado", "Flash Flood", "Hurricane (Typhoon)"); a miss comes back with the labels that year actually contains"75.00K", "1.20M", "1.00B") and are returned as both the raw cell and a parsed dollar amount. An unreported figure — about a fifth of a recent year — is omitted entirely rather than reported as 0, so it can never be read as confirmed zero damage. minDamageInUsd therefore excludes those rows and reports how many it droppednoaa_climate_get_billion_dollar_disastersQuery NOAA/NCEI's Billion-Dollar Weather and Climate Disasters — the curated record of US weather and climate disasters whose damage passed $1 billion.
summary=true for per-year counts and costs by disaster class plus an All Disasters totalDrought, Flooding, Freeze, Severe Storm, Tropical Cyclone, Wildfire, Winter Storm. disasterType rejects any other spelling rather than coercing itdeclaredCostUnit. Conflating the two would misreport by a factor of 1,000 while still looking plausible — Hurricane Helene is 78721 in a millions file, which is $78.7 billion, not $78,721 billioncoveredYears reports the span actually present (1980–2024 as of writing). A query for this year returns nothing rather than an errorstartYear/endYear match by overlap, so a disaster running across a New Year is returned from either side of itstate scope reads NCEI's per-state exports and behaves differently in two ways. Its per-event rows are national disasters that reached that state and carry the national cost, never a state share — summing states double-counts, which the response says for itself with costBasis: "national". Its per-year rows carry a binned cost range (costRangeInUsd) instead of the point estimate and 75/90/95% confidence bands the national export publishesunknown_state rather than silently falling back to national totals| Type | Name | Description |
|---|---|---|
| Resource | noaa://datasets | All CDO datasets with IDs and temporal coverage — injectable context for orienting an agent before querying data |
| Resource | noaa://stations/{stationId} | Station metadata by ID — name, coordinates, elevation, and data coverage date range |
Built on @cyanheads/mcp-ts-core:
none, jwt, oauth)in-memory, filesystem, Supabase, Cloudflare KV/R2/D1NOAA CDO-specific:
units parameter — avoids raw tenths-of-unit integer confusionlimit each report the reason CDO gave, instead of an identical bare status lineAgent-friendly output:
limit, offset, and total count in every responsereason codes and recovery hints — agents can branch on data, not string parsingAdd the following to your MCP client configuration file.
{
"mcpServers": {
"noaa-climate-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/noaa-climate-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"NOAA_CDO_TOKEN": "your-token-here"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"noaa-climate-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/noaa-climate-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"NOAA_CDO_TOKEN": "your-token-here"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"noaa-climate-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "-e", "NOAA_CDO_TOKEN=your-token-here", "ghcr.io/cyanheads/noaa-climate-mcp-server:latest"]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 NOAA_CDO_TOKEN=your-token-here bun run start:http
# Server listens at http://localhost:3010/mcp
git clone https://github.com/cyanheads/noaa-climate-mcp-server.git
cd noaa-climate-mcp-server
bun install
All configuration is validated at startup via Zod schemas in src/config/server-config.ts. Key environment variables:
| Variable | Description | Default |
|---|---|---|
NOAA_CDO_TOKEN | Required. NOAA CDO API token — obtain free at ncdc.noaa.gov/cdo-web/token | — |
MCP_TRANSPORT_TYPE | Transport: stdio or http | stdio |
MCP_HTTP_PORT | HTTP server port | 3010 |
MCP_HTTP_ENDPOINT_PATH | HTTP endpoint path where the MCP server is mounted | /mcp |
MCP_PUBLIC_URL | Public origin override for TLS-terminating reverse-proxy deployments | none |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.) | info |
MCP_GC_PRESSURE_INTERVAL_MS | Opt-in Bun-only forced-GC pressure loop (ms). Try 60000 if heap growth is observed under sustained HTTP load. | 0 (disabled) |
STORAGE_PROVIDER_TYPE | Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1 | in-memory |
OTEL_ENABLED | Enable OpenTelemetry | false |
Build and run the production version:
# One-time build
bun run rebuild
# Run the built server
bun run start:http
# or
bun run start:stdio
Run checks and tests:
bun run devcheck # Lints, formats, type-checks, and more
bun run test # Runs the test suite
bun run test:live # Opt-in: resolves every documented example identifier against the live CDO API
| Directory | Purpose |
|---|---|
src/mcp-server/tools | Tool definitions (*.tool.ts). Ten tools across datasets, locations, stations, observations, storm events, and disaster costs. |
src/mcp-server/resources | Resource definitions. Datasets catalog and station metadata resources. |
src/services/cdo | CDO HTTP client with retry, backoff, camelCase→lowercase parameter translation, and recovery of CDO's own rejection message. |
src/services/csv | Incremental RFC 4180 CSV reader shared by the two NCEI bulk-CSV corpora. |
src/services/storm-events | NCEI Storm Events bulk-CSV client — filename discovery, streamed decompression, damage parsing. |
src/services/billion-dollar-disasters | NCEI Billion-Dollar Disasters client — declared-unit resolution and conversion to whole US dollars. |
src/config | Server-specific environment variable parsing and validation with Zod. |
tests/ | Unit and integration tests, mirroring the src/ structure. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for logging, ctx.state for storagecreateApp() arraysIssues and pull requests are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
NOAA_CDO_TOKEN*NOAA CDO API token. Obtain free at https://www.ncdc.noaa.gov/cdo-web/token.
MCP_LOG_LEVELdefault: infoSets the minimum log level for output (e.g., 'debug', 'info', 'warn').
MCP_HTTP_HOSTdefault: 127.0.0.1The hostname for the HTTP server.
MCP_HTTP_PORTdefault: 3010The port to run the HTTP server on.
MCP_HTTP_ENDPOINT_PATHdefault: /mcpThe endpoint path for the MCP server.
MCP_PUBLIC_URLPublic origin override for deployments behind a TLS-terminating reverse proxy (e.g. https://mcp.example.com).
MCP_AUTH_MODEdefault: noneAuthentication mode to use: 'none', 'jwt', or 'oauth'.