Query real-time and historical USGS water data from ~8,000 stream gages and groundwater wells.
One address for any MCP client. No sign-in needed on this server.
7 read
Grouped by tool name. At most: moves money, runs code or commands, deletes data, or sends messages for you.
Odel charges nothing per call. The publisher may charge for its own service.
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.
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 markdownA public instance is available at https://usgs-water.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"usgs-water-mcp-server": {
"type": "streamable-http",
"url": "https://usgs-water.caseyjhand.com/mcp"
}
}
}
Add the following to your MCP client configuration file.
{
"mcpServers": {
"usgs-water-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/usgs-water-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"usgs-water-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/usgs-water-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"usgs-water-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"ghcr.io/cyanheads/usgs-water-mcp-server:latest"
]
}
}
}
DataCanvas is enabled by default for SQL analytics over large result sets. Set CANVAS_PROVIDER_TYPE=none to return inline previews only. The portable Claude Desktop bundle disables DataCanvas because it omits platform-specific DuckDB binaries.
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
git clone https://github.com/cyanheads/usgs-water-mcp-server.git
cd usgs-water-mcp-server
bun install
cp .env.example .env
# Edit .env to set any optional overrides
| 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.4.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.
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 build -t usgs-water-mcp-server .
docker run --rm -p 3010:3010 usgs-water-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/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.