Version License Docker MCP SDK npm TypeScript Bun
Install in Claude Desktop Install in Cursor Install in VS Code
Public Hosted Server: https://usgs-water.caseyjhand.com/mcp
Overview
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.
Tools
| 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. |
Resources
| 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
| 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.
Capability reference
water_list_parameters tool
- Without
query: the curated, built-in list — no network call — of codes like00060(Discharge, ft³/s),00065(Gage height, ft), and72019(Depth to water level, ft), filtered bygroup(streamflow,groundwater,temperature,meteorological,water-quality, orall, the default) - With
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 itssource(curatedorusgs-catalog), withtotalandtruncatedcovering the full match set.querycan't be combined with agroupother thanall - The catalog is fetched on the first query and cached for 24 hours; a failed fetch fails as a retryable
upstream_error
water_find_sites tool
- Exactly one geographic scope per call —
bbox("west,south,east,north"), 2-letterstateCd, up to 20 comma-separated 5-digit FIPScountyCdcodes, or a 2- or 8-digithuc— narrowed bysiteType(ST,GW,LK,SP, …),parameterCd, andhasDataTypeCd(iv/dv/gw). A missing or doubled scope fails asmissing_major_filter/conflicting_major_filtersbefore any NWIS call limit(1–500, default 500) andoffsetpage through the match set, withtruncatedandupstreamTotaldescribing what lies outside the window;siteOutput: expandedadds drainage and contributing area- With
CANVAS_PROVIDER_TYPE=duckdb, a match set over 500 sites also stages in full to a canvas (canvas_id/table_name) forwater_dataframe_query
water_get_readings tool
- Up to 100
sitesper call, an optionalparameterCdfilter, and aperiodlookback (ISO 8601, defaultPT2H); 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, withtotalSeries,totalValues, andtruncatedreporting what the caps held back - Every value carries its qualifier codes (
Pprovisional,Aapproved), and sites NWIS returned nothing for are named inmissingSites
water_get_series tool
- One
siteandparameterCdper call over astartDate–endDaterange;seriesTypeisdaily(one value per day, the default) orinstantaneous(~15 min). The most recent 500 records return inline, withtotalRecordsandtruncatedreporting the rest - Returns one named series — by default the daily mean and the sensor method with the most records;
statCd(00003mean,00001maximum,00002minimum; daily only) andmethodIdselect another, andotherSerieslists every alternative with its record count - With
CANVAS_PROVIDER_TYPE=duckdb, a range over 500 records spills the complete series to a canvas (canvas_id/table_name); pass a priorcanvas_idto add the table to an existing canvas
water_get_site_data_catalog tool
- One
site, optionalparameterCdandseriesType, andlimit(1–1000; default 100). Reuse the same filters and limit withnextCursoruntil it is null. An unknown site fails asnot_found; a known site with no matching metadata returnsseries: [] - Each row preserves its OGC
timeSeriesId, parameter name/unit, nullable statistic, sensor description, raw computation fields, and full UTCbeginDate/endDate.Dailymaps todaily;PointswithInstantaneousmaps toinstantaneous; other computations have nullseriesType - For
water_get_series, take YYYY-MM-DD from the bounds, passstatCdonly for daily rows, and selectmethodIdfrom the series response, never an OGC ID. Bounds may contain gaps and lag observations; null bounds remain unknown - Site identity and metadata share a 45-second deadline including retries. Malformed or rejected continuation cursors fail as
invalid_cursor; restart without cursor
water_get_conditions tool
- One
siteandparameterCd; ranks the current instantaneous reading against the period-of-record daily-mean percentiles for its calendar day as apercentileClass—record-high(≥p95),above-normal,normal,below-normal,low,record-low(<p05), orunknownfor a no-data reading — withpercentileLabelstating the threshold in plain language andcomparisonBasisnoting that the ranking is approximate, not a flood-stage or drought determination - When percentiles can't be applied, the reading still returns with
historicalContext: nulland ahistoricalContextStatusofno_record,no_matching_day,no_matching_method, orunavailable(a transient stat-service failure); on a multi-sensor site,methodIdandmethodMatchedname the sensor read and whether the percentiles came from its own series
water_dataframe_describe tool
- Lists the tables staged on a canvas by
water_get_seriesorwater_find_sites, with each column's name, DuckDB type, and nullability — call it beforewater_dataframe_queryorwater_dataframe_dropto confirm the exact names row_countis a DuckDB estimate and may differ from the exact count
water_dataframe_query tool
- A single read-only
SELECTagainst 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 restrictions - Capped at 10,000 rows per query;
truncated: truesignals more matched — narrow withWHERE/LIMIT, or runSELECT COUNT(*)for the true total
water_dataframe_drop tool
- Removes one staged table by
table_name(matched case-insensitively) from the canvas named bycanvas_id; the canvas and its other tables stay, and the response reports the dropped table'srow_countand theremaining_tables - A name not staged on the canvas fails as
table_not_foundand changes nothing, so a repeated drop is safe - Opt-in: callable only with
WATER_DATAFRAME_DROP_ENABLED=true; otherwise it appears on the landing page as a disabled tool and is absent fromtools/list. Like the other dataframe tools, it needsCANVAS_PROVIDER_TYPE=duckdb
usgs-water://site/{siteId} resource
- Returns
application/jsonsite metadata: name, coordinates, type, HUC watershed code, state, county, drainage area, and altitude siteIdis an 8–15 digit USGS site number — discover one viawater_find_sites
usgs-water://parameters resource
- The curated table of well-known parameter codes as
application/json— the listwater_list_parametersreturns without a query - Takes no parameters; a curated subset of the USGS catalog — search the rest with
water_list_parametersquery
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.
USGS Water-specific:
- Wraps NWIS IV (instantaneous), DV (daily), site, and stat endpoints plus USGS Water Data parameter codes, monitoring locations, and time-series metadata — no API key required, fully public
- Input formats are checked at the edge against what NWIS actually accepts — site numbers, parameter codes, ISO 8601 periods, HUC, FIPS county, state, and bbox each carry a validated pattern advertised in the tool's JSON Schema
- HTML error detection: NWIS returns 400 with an HTML body for bad input, and the service layer extracts NWIS's own message and maps it to a typed failure
- DataCanvas spillover:
water_get_series(long date ranges) andwater_find_sites(match sets past the 500-site cap) stage the full result as a DuckDB-backed table, queryable viawater_dataframe_query; the spillover and the dataframe tools requireCANVAS_PROVIDER_TYPE=duckdb, andwater_dataframe_dropalso needsWATER_DATAFRAME_DROP_ENABLED=true - No-data readings stay no-data: a value NWIS reports as
-999999(a seasonal, discontinued, dry, or malfunctioning gage) returns as an emptyvaluewith qualifiers naming why (Ssn,Dis,Dry,Eqp) — never as a magnitude — and stages asNULLon a canvas - Groundwater depth reads through the standard IV service via parameter
72019— the legacygwlevelsendpoint was decommissioned November 2025
Agent-friendly output:
- Percentile classification:
water_get_conditionsreturns apercentileClasscallers can act on directly, paired with apercentileLabelthat states the threshold in plain language - Partial success over hard failure:
water_get_readingsreturns the series it got and names the rest inmissingSites;water_get_conditionsseparates an empty stat table (no_record/no_matching_day) from a failed stat call (unavailable, transient) instead of collapsing both into one error - Truncation signals: every capped response (
water_get_series,water_find_sites,water_get_readings) reports its own count and atruncatedflag, pluscanvas_id/table_namewhen the rest is retrievable via SQL - Structured content and rendered text agree — every cap and count a tool applies appears identically in
structuredContentand in the markdown
Getting started
Connect 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.
Configuration
| 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.
Running the server
Local development
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
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.
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/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/. |
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 are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
Apache-2.0 — see LICENSE for details.