
Connects Claude to the USGS Instantaneous Values and Daily Values APIs covering roughly 8,000 stream gages and groundwater monitoring sites across the United States. You get seven tools: discover sites by bounding box or watershed HUC, pull current readings in near real time, fetch historical daily or instantaneous series, and check current conditions against percentile ranks from the full period of record. Large time series automatically spill to DuckDB-backed DataCanvas tables when enabled, so you can run SQL aggregations without truncation. Useful for hydrologic analysis, drought and flood assessment, or any workflow that needs authoritative water data with geographic and temporal filtering. Supports both stdio and streamable HTTP transports, with a public hosted instance available.
Sponsors
USGS_USER_AGENTCustom User-Agent string for USGS NWIS requests. Defaults to a descriptive identifier including the repo URL.
USGS_REQUEST_TIMEOUT_MSdefault: 30000HTTP request timeout in milliseconds for NWIS calls. Default: 30000.
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_AUTH_MODEdefault: noneAuthentication mode to use: 'none', 'jwt', or 'oauth'.
Query real-time and historical water data from ~8,000 USGS stream gages and groundwater wells via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://usgs-water.caseyjhand.com/mcp
Real-time and historical water data from ~8,000 active USGS stream gages and groundwater wells across the US and territories. Find monitoring sites, pull the latest readings or a historical series, and see how today's flow compares with decades of records.
| Tool | Description |
|---|---|
water_list_parameters | List well-known USGS parameter codes with names, units, and domain (no network call), or search the full ~19,600-code USGS catalog by name or description. |
water_find_sites | Find USGS monitoring sites by bounding box, state, county, or HUC watershed. Filter by site type and parameter availability. Large match sets spill to DataCanvas. |
water_get_readings | Get the latest instantaneous values (~15 min real-time) for up to 100 USGS sites. |
water_get_series | Get a time series of daily or instantaneous values for a site over a date range. Large ranges spill to DataCanvas. |
water_get_site_data_catalog | Discover a site's parameters, sensors, statistics, and full UTC record bounds, with cursor pagination. |
water_get_conditions | Get current hydrologic conditions ranked against the full period-of-record percentile statistics. |
water_dataframe_describe | List tables and columns staged on a DataCanvas by water_get_series or water_find_sites. |
water_dataframe_query | Run a read-only SQL SELECT against the time-series and site tables staged by water_get_series and water_find_sites. |
water_dataframe_drop | Drop one staged table from a DataCanvas, leaving the canvas and its other tables in place. Opt-in via WATER_DATAFRAME_DROP_ENABLED. |
| Resource | Description |
|---|---|
usgs-water://site/{siteId} | Site metadata: name, coordinates, type, HUC, state, county, drainage area, and altitude |
usgs-water://parameters | Curated well-known parameter codes (the list water_list_parameters returns without a query) |
All resource data is also reachable via tools. Use water_find_sites for geographic site discovery.
| Skill | Description |
|---|---|
usgs-water-guide | Gates and workflows for agents calling the tools: telling live gages from discontinued ones, reading depth-to-water rankings (they run inverted), finding the parameter code a site reports, time zones and data lag, and SQL over staged series |
Ships in skills/ for plugin installs, the npm package, the Docker image, and the .mcpb bundle, and is served over MCP as skill://usgs-water-guide/SKILL.md.
water_list_parameters toolquery: the curated, built-in list — no network call — of codes like 00060 (Discharge, ft³/s), 00065 (Gage height, ft), and 72019 (Depth to water level, ft), filtered by group (streamflow, groundwater, temperature, meteorological, water-quality, or all, the default)query: searches the full ~19,600-code USGS catalog by name or description (a bare 5-digit code returns that code) and returns at most 25 entries, each naming its source (curated or usgs-catalog), with total and truncated covering the full match set. query can't be combined with a group other than allupstream_errorwater_find_sites toolbbox ("west,south,east,north"), 2-letter stateCd, up to 20 comma-separated 5-digit FIPS countyCd codes, or a 2- or 8-digit huc — narrowed by siteType (ST, GW, LK, SP, …), parameterCd, and hasDataTypeCd (iv / dv / gw). A missing or doubled scope fails as missing_major_filter / conflicting_major_filters before any NWIS calllimit (1–500, default 500) and offset page through the match set, with truncated and upstreamTotal describing what lies outside the window; siteOutput: expanded adds drainage and contributing areaCANVAS_PROVIDER_TYPE=duckdb, a match set over 500 sites also stages in full to a canvas (canvas_id / table_name) for water_dataframe_querywater_get_readings toolsites per call, an optional parameterCd filter, and a period lookback (ISO 8601, default PT2H); returns one series per site, parameter, and sensor method (methodId / methodDescription), capped at 100 series per call (kept round-robin across sites) and the 10 most recent records per series, with totalSeries, totalValues, and truncated reporting what the caps held backP provisional, A approved), and sites NWIS returned nothing for are named in missingSiteswater_get_series toolsite and parameterCd per call over a startDate–endDate range; seriesType is daily (one value per day, the default) or instantaneous (~15 min). The most recent 500 records return inline, with totalRecords and truncated reporting the reststatCd (00003 mean, 00001 maximum, 00002 minimum; daily only) and methodId select another, and otherSeries lists every alternative with its record countCANVAS_PROVIDER_TYPE=duckdb, a range over 500 records spills the complete series to a canvas (canvas_id / table_name); pass a prior canvas_id to add the table to an existing canvaswater_get_site_data_catalog toolsite, optional parameterCd and seriesType, and limit (1–1000; default 100). Reuse the same filters and limit with nextCursor until it is null. An unknown site fails as not_found; a known site with no matching metadata returns series: []timeSeriesId, parameter name/unit, nullable statistic, sensor description, raw computation fields, and full UTC beginDate/endDate. Daily maps to daily; Points with Instantaneous maps to instantaneous; other computations have null seriesTypewater_get_series, take YYYY-MM-DD from the bounds, pass statCd only for daily rows, and select methodId from the series response, never an OGC ID. Bounds may contain gaps and lag observations; null bounds remain unknowninvalid_cursor; restart without cursorwater_get_conditions toolsite and parameterCd; ranks the current instantaneous reading against the period-of-record daily-mean percentiles for its calendar day as a percentileClass — record-high (≥p95), above-normal, normal, below-normal, low, record-low (<p05), or unknown for a no-data reading — with percentileLabel stating the threshold in plain language and comparisonBasis noting that the ranking is approximate, not a flood-stage or drought determinationhistoricalContext: null and a historicalContextStatus of no_record, no_matching_day, no_matching_method, or unavailable (a transient stat-service failure); on a multi-sensor site, methodId and methodMatched name the sensor read and whether the percentiles came from its own serieswater_dataframe_describe toolwater_get_series or water_find_sites, with each column's name, DuckDB type, and nullability — call it before water_dataframe_query or water_dataframe_drop to confirm the exact namesrow_count is a DuckDB estimate and may differ from the exact countwater_dataframe_query toolSELECT against the staged tables; other statements, system-catalog access (information_schema, pg_catalog, duckdb_*), engine-setting functions, dynamic SQL, and file references are rejected. query() accepts a single SQL string literal, checked under the same restrictionstruncated: true signals more matched — narrow with WHERE/LIMIT, or run SELECT COUNT(*) for the true totalwater_dataframe_drop tooltable_name (matched case-insensitively) from the canvas named by canvas_id; the canvas and its other tables stay, and the response reports the dropped table's row_count and the remaining_tablestable_not_found and changes nothing, so a repeated drop is safeWATER_DATAFRAME_DROP_ENABLED=true; otherwise it appears on the landing page as a disabled tool and is absent from tools/list. Like the other dataframe tools, it needs CANVAS_PROVIDER_TYPE=duckdbusgs-water://site/{siteId} resourceapplication/json site metadata: name, coordinates, type, HUC watershed code, state, county, drainage area, and altitudesiteId is an 8–15 digit USGS site number — discover one via water_find_sitesusgs-water://parameters resourceapplication/json — the list water_list_parameters returns without a querywater_list_parameters queryBuilt 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.
USGS Water-specific:
water_get_series (long date ranges) and water_find_sites (match sets past the 500-site cap) stage the full result as a DuckDB-backed table, queryable via water_dataframe_query; the spillover and the dataframe tools require CANVAS_PROVIDER_TYPE=duckdb, and water_dataframe_drop also needs WATER_DATAFRAME_DROP_ENABLED=true-999999 (a seasonal, discontinued, dry, or malfunctioning gage) returns as an empty value with qualifiers naming why (Ssn, Dis, Dry, Eqp) — never as a magnitude — and stages as NULL on a canvas72019 — the legacy gwlevels endpoint was decommissioned November 2025Agent-friendly output:
water_get_conditions returns a percentileClass callers can act on directly, paired with a percentileLabel that states the threshold in plain languagewater_get_readings returns the series it got and names the rest in missingSites; water_get_conditions separates an empty stat table (no_record / no_matching_day) from a failed stat call (unavailable, transient) instead of collapsing both into one errorwater_get_series, water_find_sites, water_get_readings) reports its own count and a truncated flag, plus canvas_id / table_name when the rest is retrievable via SQLstructuredContent and in the markdownConnect to the hosted instance, no install:
claude mcp add --transport http usgs-water-mcp-server https://usgs-water.caseyjhand.com/mcp
Or run it locally:
claude mcp add usgs-water-mcp-server -- npx -y @cyanheads/usgs-water-mcp-server
For other clients, add this to the MCP config file:
{
"mcpServers": {
"usgs-water-mcp-server": {
"command": "npx",
"args": ["-y", "@cyanheads/usgs-water-mcp-server"]
}
}
}
DataCanvas is enabled by default for SQL analytics over large result sets; set CANVAS_PROVIDER_TYPE=none to return inline previews only. No API key is required — USGS NWIS is a free, public API. For Docker or a source build, see Running the server.
| Variable | Description | Default |
|---|---|---|
CANVAS_PROVIDER_TYPE | DataCanvas spillover for large results from water_get_series and water_find_sites; set to none to disable. The portable desktop bundle sets none. | duckdb |
USGS_USER_AGENT | Custom User-Agent string sent to USGS NWIS and OGC discovery endpoints. USGS requests a descriptive User-Agent per their terms. | usgs-water-mcp-server/0.5.0 (contact: https://github.com/cyanheads/usgs-water-mcp-server) |
USGS_REQUEST_TIMEOUT_MS | HTTP request timeout in milliseconds for USGS NWIS and OGC calls. Site-series discovery also has a shared 45-second deadline. | 30000 |
WATER_DATAFRAME_DROP_ENABLED | Set to true to make water_dataframe_drop callable. Off, the tool is listed as disabled. Needs CANVAS_PROVIDER_TYPE=duckdb to have anything to drop. | false |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for HTTP server. | 3010 |
MCP_SESSION_MODE | HTTP session mode. The server declares stateless in code, matching .env.example and the Docker runtime; setting this overrides that, and auto resolves to stateful. | stateless |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
LOG_TOOL_FAILURE_PAYLOADS | Log each failed tool call's arguments and result, redacted by key name and capped at LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES (default 16384). A secret inside a free-form value is not redacted. | false |
OTEL_ENABLED | Enable OpenTelemetry instrumentation (spans, metrics, completion logs). | false |
See .env.example for the full list of optional overrides.
Requires Bun v1.4.0 or higher (or Node.js v24+).
Clone and install:
git clone https://github.com/cyanheads/usgs-water-mcp-server.git
cd usgs-water-mcp-server && bun install
cp .env.example .env # optional overrides
Build and run:
# One-time build
bun run rebuild
# Run the built server
bun run start:stdio
# or
bun run start:http
Run checks and tests:
bun run devcheck # Lint, format, typecheck, security
bun run test # Vitest test suite
bun run lint:mcp # Validate MCP definitions against spec
docker run --rm -p 3010:3010 ghcr.io/cyanheads/usgs-water-mcp-server:latest
The image defaults to HTTP transport, stateless session mode, and logs to /var/log/usgs-water-mcp-server. To run it over stdio from an MCP client, use "command": "docker" with "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/usgs-water-mcp-server:latest"]. To build locally, docker build -t usgs-water-mcp-server .; OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools/resources and inits services. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts). |
src/mcp-server/resources | Resource definitions (*.resource.ts). |
src/services/nwis | NWIS HTTP client — IV, DV, site, and stat endpoints with HTML error detection. |
src/services/waterdata | USGS Water Data site-series discovery, parameter-code catalog reader (24-hour cache), and curated parameter table. |
src/services/canvas | DataCanvas accessor, caller-supplied canvas_id resolution, and deterministic table names for DuckDB-backed spillover. |
tests/ | Unit and integration tests mirroring src/. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped logging, ctx.state for tenant-scoped storagesrc/mcp-server/*/definitions/index.tsIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.
@cyanheads/usgs-water-mcp-server