@cyanheads/usgs-water-mcp-server
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
Tools
Five tools for querying USGS water data, plus two for SQL analytics over the DuckDB-backed canvas dataframes that water_get_series materializes:
| Tool | Description |
|---|---|
water_list_parameters | Static lookup of well-known USGS parameter codes with names, units, and domain. No network call. |
water_find_sites | Find USGS monitoring sites by bounding box, state, county, or HUC watershed. Filter by site type and parameter availability. |
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_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. |
water_dataframe_query | Run a read-only SQL SELECT against time-series tables staged by water_get_series. |
water_list_parameters
Static lookup of well-known USGS parameter codes — no network call, instant response.
- Discover that
00060= Discharge (ft³/s),00065= Gage height (ft),00010= Temperature (°C),72019= Depth to water level (ft), and more - Filter by thematic domain:
streamflow,groundwater,temperature,meteorological,water-quality, orall - Use this first — parameter codes are required by every other water tool
water_find_sites
Discover USGS monitoring sites before calling data tools — all other tools require a site number.
- Geographic scoping: bounding box (
"west,south,east,north"decimal degrees), state code, FIPS county code, or HUC watershed code - Site type filtering:
ST(stream),GW(groundwater well),LK(lake/reservoir),SP(spring), and more - Parameter filter: only return sites that have data for a specific parameter code
- Data type filter: require sites with real-time (
iv), daily (dv), or groundwater (gw) data - Returns site number, name, coordinates, type, state/county/HUC codes, and available data types
water_get_readings
Get the latest instantaneous (~15 min) values for one or more USGS monitoring sites.
- Batch up to 100 site numbers in a single call
- Accepts any parameter code discoverable via
water_list_parameters - Configurable lookback period via ISO 8601 duration (e.g.
PT2H= last 2 hours,P7D= last 7 days) - Returns per-site, per-parameter records with timestamp, value, unit, and provisional/approved qualifier
- Groundwater depth available via
parameterCd=72019(the legacygwlevelsendpoint was decommissioned November 2025 — use the IV service instead)
water_get_series
Get a historical time series for a site and parameter over a date range.
- Daily values (DV service, one value per day) or instantaneous values (IV service, ~15 min resolution)
- Returns site name, parameter name, unit code, and time-ordered value records with qualifiers
- DataCanvas spillover: large date ranges (>500 records) automatically spill to a DuckDB-backed canvas when
CANVAS_PROVIDER_TYPE=duckdbis set — response includescanvas_idandtable_namefor follow-up SQL viawater_dataframe_query - Without DataCanvas, returns the most recent 500 records with a
truncatedflag andtotalRecordscount - Supports chaining: pass a prior
canvas_idto append data to an existing canvas
water_get_conditions
Get current hydrologic conditions placed in full historical context.
- Fetches the current IV reading and the full daily percentile table in parallel
- Classifies the reading:
record-high(≥ p95),above-normal(p75–p95),normal(p25–p75),below-normal(p10–p25),low(p05–p10),record-low(< p05) - Answers "is this flooding or drought?" — not just a raw number
- Gracefully degrades when a site has insufficient record history: returns the current reading with
historicalContext: nulland an explanatory note
water_dataframe_describe / water_dataframe_query
In-conversation SQL analytics over the time-series dataframes that water_get_series materializes on a DuckDB-backed canvas.
Workflow:
- Call
water_get_serieswith a large date range — when DataCanvas is enabled, the response includescanvas_idandtable_name - Call
water_dataframe_describewith thecanvas_idto confirm the table schema (columns:date_time,value,qualifiers,site_number,parameter_cd,unit_code) - Call
water_dataframe_querywith thecanvas_idand a SELECT statement to run aggregates, filter by qualifier, or join multiple series
Read-only by default — only SELECT statements are permitted. Results are capped at 10,000 rows. Requires CANVAS_PROVIDER_TYPE=duckdb in the server environment.
Resources and prompts
| Type | Name | Description |
|---|---|---|
| Resource | usgs-water://site/{siteId} | Site metadata: name, coordinates, type, HUC, state, county, and available data types |
| Resource | usgs-water://parameters | Full parameter code catalog (same data as water_list_parameters) |
All resource data is also reachable via tools. Use water_find_sites for geographic site discovery.
Features
Built on @cyanheads/mcp-ts-core:
- Declarative tool and resource definitions — single file per primitive, framework handles registration and validation
- Unified error handling — handlers throw, framework catches, classifies, and formats
- Pluggable auth:
none,jwt,oauth - Swappable storage backends:
in-memory,filesystem,Supabase,Cloudflare KV/R2/D1 - Structured logging with optional OpenTelemetry tracing
- STDIO and Streamable HTTP transports
USGS NWIS–specific:
- Wraps NWIS IV (instantaneous), DV (daily), site, and stat endpoints — no API key required, fully public
- HTML error detection: NWIS returns 400 with an HTML body for bad inputs; the service layer extracts the error message and maps it to a typed failure
- Multi-site batching:
water_get_readingsaccepts up to 100 site numbers in one call - Provisional vs. approved data qualifiers surfaced on every reading — not hidden from callers
- DataCanvas spillover:
water_get_seriesmaterializes large date-range responses as DuckDB-backeddf_<id>tables queryable viawater_dataframe_query - Groundwater via the IV service using parameter
72019— the legacygwlevelsendpoint was decommissioned November 2025
Agent-friendly output:
- Percentile classification on every conditions response — callers get a
percentileClassstring (record-high,normal,record-low, etc.) they can act on directly without parsing numeric thresholds - Partial success on conditions: missing stat data returns
historicalContext: nullwith an explanatory note rather than an error, so the current reading is always available when the site is valid - Truncation signals:
water_get_seriesalways reportstotalRecordsandtruncatedso callers know when a preview is incomplete, andcanvas_id/table_nametell them exactly how to retrieve the rest
Getting started
Public Hosted Instance
A 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"
}
}
}
Self-Hosted / Local
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"
]
}
}
}
To enable DataCanvas for SQL analytics over large time-series results, add CANVAS_PROVIDER_TYPE=duckdb to the env block in any of the configs above.
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
Prerequisites
- Bun v1.3.11 or higher (or Node.js v24+).
- No API key required — USGS NWIS is a free, public API.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/usgs-water-mcp-server.git
- Navigate into the directory:
cd usgs-water-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# Edit .env to set any optional overrides
Configuration
| Variable | Description | Default |
|---|---|---|
CANVAS_PROVIDER_TYPE | Set to duckdb to enable DataCanvas spillover for large time-series results from water_get_series. | — |
USGS_USER_AGENT | Custom User-Agent string sent to USGS NWIS. USGS requests a descriptive User-Agent per their terms. | usgs-water-mcp-server/0.1.5 (contact: https://github.com/cyanheads/usgs-water-mcp-server) |
USGS_REQUEST_TIMEOUT_MS | HTTP request timeout in milliseconds for NWIS calls. | 30000 |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for HTTP server. | 3010 |
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 |
OTEL_ENABLED | Enable OpenTelemetry instrumentation (spans, metrics, completion logs). | false |
See .env.example for the full list of optional overrides.
Running the server
Local development
-
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
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.
Project structure
| 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/canvas | DataCanvas accessor for DuckDB-backed spillover. |
tests/ | Unit and integration tests mirroring src/. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storage - Register new tools and resources via the barrels in
src/mcp-server/*/definitions/index.ts - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Contributing
Issues and pull requests are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
Apache-2.0 — see LICENSE for details.