Search NOAA CDO stations and datasets, fetch historical weather observations.
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
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 |
| 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 |
noaa_climate_list_datasets toollimit 1–1000, default 25; offset), sortable by id, name, mindate, maxdate, or datacoveragenoaa_climate_list_data_categories toollimit 1–1000, default 25; offset), sortable by id or namenoaa_climate_list_data_types to narrow by measurement domainnoaa_climate_list_data_types tooldatasetId (e.g. GHCND) or datacategoryId (e.g. TEMP) — hundreds of types exist across all datasetsTMAX, TMIN, PRCP, SNOW, SNWD, AWNDlimit 1–1000, default 25; offset)noaa_climate_list_location_categories toolnoaa_climate_find_locations accepts as locationCategoryId: CITY, ST, CNTY, CNTRY, ZIP, US_TERR, CLIM_REG, CLIM_DIV, HYD_ACC, HYD_CAT, HYD_REG, HYD_SUBid or name; paginated (limit 1–1000, default 25; offset)noaa_climate_find_locations toollocationCategoryId scopes the search (e.g. ST returns all 51 states in one call); omit it to return every location typenameContains 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 limitnoaa_climate_find_stations and noaa_climate_fetch_data — FIPS:37, CITY:US530018, ZIP:98101nameContains is passed without locationCategoryId, or the resolved category is too large to enumeratelimit 1–1000, default 25; offset); sort alphabetically by name to page through an over-large category insteadnoaa_climate_find_stations toollocationId, extent (lat/lon bounding box), datasetId, datatypeId (array), and date rangenoaa_climate_fetch_data as stationIddatasetId and date range to confirm a returned station actually has data for what you plan to queryGHCND:USW00024233, COOP:010008limit 1–1000, default 25; offset)noaa_climate_get_station toolstationIdnoaa://stations/{stationId} resource as a direct callnot_found when the ID is well-formed but resolves to nothingnoaa_climate_fetch_data tooldatasetId, startDate, endDate; optional stationId, locationId, datatypeId filters (arrays)startDateunits: "metric" or "standard" is strongly recommended — without it, GHCND values are raw tenths-of-unit integers (e.g. TMAX=256 is 25.6°C)NORMAL_* dataset, use startDate=2010-01-01 / endDate=2010-12-31 — the fixed API proxy year regardless of which 30-year period is describeddate_range_exceeded reports the exact maxEndDate CDO will accept; an unrecognized datasetId fails validation_error before any network call{ date, datatype, station, value, attributes } tuples plus an effectiveQuery echo of the applied filtersnoaa_climate_search_storm_events toolyear is required (1950 through the current partial year, one file per year)state (the full NCEI name, e.g. "FLORIDA", never a postal code), eventType (matched case-insensitively against the exact NWS label), month, and minDamageInUsd"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 droppedlimit 1–100 (default 50) with offset; a zero-match response names the event types and states the requested year actually containsyear_unavailable when NCEI has no file for the year; malformed_export if a downloaded file fails to decompress into the expected tablenoaa_climate_get_billion_dollar_disasters toolsummary=true for per-year counts and costs by disaster class plus an "All Disasters" totaldeclaredCostUnitstartYear/endYear (overlap match), disasterType (one of seven exact NCEI classes), minCostInUsd, and state (two-letter postal code)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 bandscoveredYears), not the current calendar year; limit 1–100 (default 50) with offsetnoaa://datasets resourceapplication/json — IDs, names, temporal coveragenoaa_climate_list_datasets with no filters and a high limit — injectable, zero-fetch contextnoaa://stations/{stationId} resourcenoaa_climate_get_stationstationId comes from noaa_climate_find_stationsnot_found when the ID is well-formed but resolves to nothingBuilt 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:
units parameter avoids raw tenths-of-unit integer confusionlimit reports the reason CDO gave instead of a bare status lineAgent-friendly output:
limit, offset, and total count in every responsereason codes and recovery hints — agents branch on data, not string parsingA 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"
}
}
}
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
# 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
cp .env.example .env
# edit .env and set required vars
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.
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
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.
| 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 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.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
npx -y @cyanheads/noaa-cdo-mcp-serverMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-cyanheads-noaa-cdo-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/noaa-cdo-mcp-server"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceio.github.cyanheads/noaa-cdo-mcp-server works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.