@cyanheads/noaa-climate-mcp-server
Search NOAA climate stations and datasets, fetch historical weather observations via MCP. STDIO or Streamable HTTP.
10 Tools β’ 2 Resources
Overview
NOAA Climate Data Online (CDO) API v2 for historical weather observations, plus two separate NCEI bulk-CSV corpora β the Storm Events Database and Billion-Dollar Weather and Climate Disasters. Search locations and stations, fetch historical observations with date-range validation and unit conversion, and query severe-weather events or disaster costs from any MCP client. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
| 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 |
Resources
| Resource | Description |
|---|
noaa://datasets | All CDO datasets with IDs and temporal coverage β injectable context for orienting an agent before querying data |
noaa://stations/{stationId} | Station metadata by ID β name, coordinates, elevation, and data coverage date range |
Capability reference
- No required parameters β returns all ~11 CDO datasets by default; optional filters by data type, location, station, or date range
- Paginated (
limit 1β1000, default 25; offset), sortable by id, name, mindate, maxdate, or datacoverage
- Common IDs: GHCND (daily, 1763βpresent), GSOM (monthly), GSOY (annual), NORMAL_DLY/MLY/ANN/HLY (1981β2010 normals)
- 42 categories total (Temperature, Precipitation, Wind, Pressure, Sunshine, Sky cover, Weather Type, and more)
- Optional filters by dataset, location, station, or date range; paginated (
limit 1β1000, default 25; offset), sortable by id or name
- Use before
noaa_climate_list_data_types to narrow by measurement domain
- Filter by
datasetId (e.g. GHCND) or datacategoryId (e.g. TEMP) β hundreds of types exist across all datasets
- Common GHCND types:
TMAX, TMIN, PRCP, SNOW, SNWD, AWND
- Coverage fraction and date range are included only when the upstream record carries them
- Paginated (
limit 1β1000, default 25; offset)
- Returns the 12 category IDs
noaa_climate_find_locations accepts as locationCategoryId: CITY, ST, CNTY, CNTRY, ZIP, US_TERR, CLIM_REG, CLIM_DIV, HYD_ACC, HYD_CAT, HYD_REG, HYD_SUB
- Sortable by
id or name; paginated (limit 1β1000, default 25; offset)
- Pagination and sort only β CDO ignores dataset, location, station, and date filters on this endpoint, so none are offered
locationCategoryId scopes the search (e.g. ST returns all 51 states in one call); omit it to return every location type
nameContains synthesizes the name search CDO lacks by enumerating the category client-side and matching the substring case-insensitively β capped to categories of at most 4,000 locations (every category but ZIP, 30,415); a datasetId/datacategoryId filter can narrow a larger category under that limit
- Returns location IDs used by
noaa_climate_find_stations and noaa_climate_fetch_data β FIPS:37, CITY:US530018, ZIP:98101
- Typed failures when
nameContains is passed without locationCategoryId, or the resolved category is too large to enumerate
- Paginated (
limit 1β1000, default 25; offset); sort alphabetically by name to page through an over-large category instead
- Filter by
locationId, extent (lat/lon bounding box), datasetId, datatypeId (array), and date range
- Returns station IDs, names, coordinates, elevation, and data-coverage dates β station IDs feed
noaa_climate_fetch_data as stationId
- Pair
datasetId and date range to confirm a returned station actually has data for what you plan to query
- Common station ID formats:
GHCND:USW00024233, COOP:010008
- Paginated (
limit 1β1000, default 25; offset)
- Single required input:
stationId
- Returns name, coordinates, elevation, and full data-coverage date range
- Mirrors the
noaa://stations/{stationId} resource as a direct call
not_found when the ID is well-formed but resolves to nothing
- Requires
datasetId, startDate, endDate; optional stationId, locationId, datatypeId filters (arrays)
- Date-range cap depends on dataset: GHCND, PRECIP_15, PRECIP_HLY, NORMAL_DLY, NORMAL_HLY, NEXRAD2, NEXRAD3 allow 1 year max; GSOM, GSOY, NORMAL_MLY, NORMAL_ANN allow 10 years max, measured to the end of the calendar month that many years after
startDate
units: "metric" or "standard" is strongly recommended β without it, GHCND values are raw tenths-of-unit integers (e.g. TMAX=256 is 25.6Β°C)
- For any
NORMAL_* dataset, use startDate=2010-01-01 / endDate=2010-12-31 β the fixed API proxy year regardless of which 30-year period is described
date_range_exceeded reports the exact maxEndDate CDO will accept; an unrecognized datasetId fails validation_error before any network call
- Returns flat
{ date, datatype, station, value, attributes } tuples plus an effectiveQuery echo of the applied filters
- Separate NCEI bulk-CSV corpus β no token required;
year is required (1950 through the current partial year, one file per year)
- Filter by
state (the full NCEI name, e.g. "FLORIDA", never a postal code), eventType (matched case-insensitively against the exact NWS label), month, and minDamageInUsd
- Damage arrives as both the raw magnitude-suffixed string (
"1.20M") and a parsed dollar amount; an unreported figure is omitted rather than reported as zero, and minDamageInUsd excludes those rows and reports how many it dropped
limit 1β100 (default 50) with offset; a zero-match response names the event types and states the requested year actually contains
year_unavailable when NCEI has no file for the year; malformed_export if a downloaded file fails to decompress into the expected table
- Two shapes: individual disasters by default, or
summary=true for per-year counts and costs by disaster class plus an "All Disasters" total
- Every cost is normalized to whole US dollars regardless of the unit NCEI declares per export (millions for the per-event file, billions for the national per-year file); the response echoes the source unit as
declaredCostUnit
- Filter by
startYear/endYear (overlap match), disasterType (one of seven exact NCEI classes), minCostInUsd, and state (two-letter postal code)
- A
state scope reports each disaster's national cost, not a state share (costBasis: "national"), and its per-year rows carry a binned costRangeInUsd instead of a point estimate and confidence bands
- Coverage runs 1980 through the last year NCEI has finished assessing (
coveredYears), not the current calendar year; limit 1β100 (default 50) with offset
noaa://datasets resource
- All CDO datasets as
application/json β IDs, names, temporal coverage
- Equivalent to
noaa_climate_list_datasets with no filters and a high limit β injectable, zero-fetch context
noaa://stations/{stationId} resource
- Station metadata by ID β mirrors
noaa_climate_get_station
stationId comes from noaa_climate_find_stations
not_found when the ID is well-formed but resolves to nothing
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
NOAA-specific:
- Full CDO API v2 coverage β datasets, data categories, data types, locations, stations, and observations β plus two separate NCEI bulk-CSV corpora requiring no token: severe-weather events (Storm Events Database) and billion-dollar disaster costs
- Client-side date-range validation enforced per dataset before hitting the API, reporting the exact upstream limit back to the caller
- Unit normalization via CDO's
units parameter avoids raw tenths-of-unit integer confusion
- CDO's own rejection message is recovered and surfaced β an over-long date range, malformed date, missing parameter, or over-large
limit reports the reason CDO gave instead of a bare status line
- Billion-Dollar Disasters costs are converted to whole US dollars from whichever unit each NCEI export declares in its own preamble, since the unit differs by export
Agent-friendly output:
- Paginated results across every list and search tool β
limit, offset, and total count in every response
- Station, location, and dataset IDs flow naturally between tools β find a location, find stations in it, fetch data from those stations
- Structured error contracts with typed
reason codes and recovery hints β agents branch on data, not string parsing
- Damage and cost fields are honest about upstream gaps β an unreported NCEI figure is omitted rather than reported as a confirmed zero
Getting started
Public Hosted Instance
A public instance is available at https://noaa-climate.caseyjhand.com/mcp β no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"noaa-climate-mcp-server": {
"type": "streamable-http",
"url": "https://noaa-climate.caseyjhand.com/mcp"
}
}
}
Self-Hosted / Local
Add 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
Prerequisites
Installation
- Clone the repository:
git clone https://github.com/cyanheads/noaa-climate-mcp-server.git
- Navigate into the directory:
cd noaa-climate-mcp-server
- Install dependencies:
- Configure environment:
Configuration
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_SESSION_MODE | HTTP session posture: stateful, stateless, or auto. Ships as stateless β no tool asks the caller for input mid-handler | stateless |
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 |
See .env.example for the full list of optional overrides.
Running the server
Local development
Docker
docker build -t noaa-climate-mcp-server .
docker run --rm -e NOAA_CDO_TOKEN=your-token-here -p 3010:3010 noaa-climate-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/noaa-climate-mcp-server. OpenTelemetry peer dependencies are installed by default β build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
| 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. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches β no
try/catch in tool logic
- Use
ctx.log for logging, ctx.state for storage
- Register new tools and resources in the
createApp() arrays
- Wrap external API calls as validate raw β normalize to domain types β return the output schema; never fabricate missing fields
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.