# io.github.cyanheads/usgs-water-mcp-server

> Query real-time and historical USGS water data from ~8,000 stream gages and groundwater wells.

- name: io.github.cyanheads/usgs-water-mcp-server
- version: 0.4.0
- connection class: R0 (autonomous)
- trust: 94/100
- quality: 70/100 (needs work)
- owner: not claimed
- descriptor: https://api.protogrid.dev/v1/servers/io.github.cyanheads%2Fusgs-water-mcp-server
- tools: https://api.protogrid.dev/v1/servers/io.github.cyanheads%2Fusgs-water-mcp-server/tools

**An agent can connect right now, no human step.** Remote endpoint, no authentication, reachable on the last probe.

## Connection (mcpServers)

```json
{
  "mcpServers": {
    "usgs-water-mcp-server": {
      "type": "http",
      "url": "https://usgs-water.caseyjhand.com/mcp"
    }
  }
}
```


## Trust

- hygiene: 100
- liveness: 100
- freshness: 100
- provenance: 80
- drivers: +repository +namespace-matches-repo +reachable +uptime-100% +updated-1d-ago
- Derived from observable signals (official registry feed and our own credential-free probes); no code audit performed.

## Quality 70/100 (needs work)

### protocol: 88 (weight 25)

- pass `protocol.modern`: supports 2026-07-28 (server/discover)
- pass `protocol.stateless`: answers without a session
- pass `protocol.transport`: streamable HTTP
- warn `protocol.list_ttl`: tools/list declares ttlMs 0, so clients re-fetch it on every use

### authorization: n/a (weight 15)

- n/a `auth.prm`, `auth.as_metadata`, `auth.cimd`: no remote uses OAuth
- n/a `auth.secret_in_url`: no templated URL

### tool hygiene: 79 (weight 30)

- pass `tools.descriptions`: every tool has a description
- warn `tools.description_length`: 1 tools have descriptions over 1,024 characters, which crowds the context
- pass `tools.schemas`: every tool has an object input schema
- pass `tools.annotations`: 8 of 8 tools declare readOnlyHint or destructiveHint
- fail `tools.directory_hints`: no tool declares all four of readOnlyHint, destructiveHint, idempotentHint and openWorldHint; missing: water_find_sites (destructiveHint); water_list_parameters (destructiveHint); water_get_readings (destructiveHint); …
- pass `tools.token_cost`: about 4,311 tokens to load every tool
- pass `tools.api_dump`: tools are not a one-to-one API dump

### stability: 25 (weight 15)

- warn `stability.changes`: 11 tool changes in 30 days
- fail `stability.rug_pull`: 1 tools kept their name but changed most of their description in 30 days; review before trusting them

### dependencies: n/a (weight 15)

- n/a `deps.known_vulns`, `deps.mcp_sdk_version`, `deps.resolvable`: deps.dev does not know @cyanheads/usgs-water-mcp-server@0.4.0, @cyanheads/usgs-water-mcp-server@0.4.0

- tools: 8, about 4,311 tokens to load them all
- tool set hash: 76279f83499fc1e5c7f2a3300a41b519
- tools last changed: 2026-10-20T10:22:50.925Z
- badge: [![protogrid quality](https://protogrid.dev/badge/io.github.cyanheads/usgs-water-mcp-server.svg)](https://protogrid.dev/servers/io.github.cyanheads/usgs-water-mcp-server)
- Checks run on what our credential-free, read-only probes observe; tools are never called and no code audit is performed.

### Tool changes

- 2026-10-20 `water_get_conditions` changed (outputSchema)
- 2026-10-20 `water_get_site_data_catalog` added
  - Discover a USGS site’s parameters, sensors, statistics, and UTC record bounds before calling water_get_series. Find site numbers with water_find_sites. Each row is a distinct series; metadata bounds may contain gaps and lag observations. For water_get_series, take YYYY-MM-DD from beginDate/endDate, pass statCd only for daily rows, and obtain methodId from a water_get_series response; timeSeriesId is an OGC identifier, never an NWIS methodId. Rows with null seriesType are other computations and cannot be requested with water_get_series. Follow nextCursor with the same filters and limit until null.
- 2026-10-20 `water_get_series` changed (outputSchema)
- 2026-10-20 `water_get_readings` changed (outputSchema)
- 2026-10-20 `water_find_sites` changed (outputSchema)
- 2026-10-04 `water_dataframe_describe` changed (description)
  - before: List tables and columns staged on a DataCanvas by water_get_series or water_find_sites. Call this after water_get_series or water_find_sites returns a canvas_id to discover the exact table name and column types before writing a query. Then pass the table name to water_dataframe_query. Requires DataCanvas to be enabled on this server instance. Returns an error if DataCanvas is not available.
  - after: List tables and columns staged on a DataCanvas by water_get_series or water_find_sites. Call this after water_get_series or water_find_sites returns a canvas_id to discover the exact table name and column types before writing a query. Then pass the table name to water_dataframe_query, or to water_dataframe_drop (when this server enables it) to remove a table you no longer need. Requires DataCanvas to be enabled on this server instance. Returns an error if DataCanvas is not available.
- 2026-10-04 `water_dataframe_query` changed (description)
  - before: Run a read-only SQL SELECT against water data tables staged on a DataCanvas by water_get_series or water_find_sites. Workflow: run water_get_series or water_find_sites (get canvas_id + table_name) → water_dataframe_describe (confirm the table and its columns) → water_dataframe_query (SQL analysis). Only SELECT statements are permitted. At most 10,000 rows are returned; a query matching more is capped and the response sets truncated=true — scope with WHERE/LIMIT, and use SELECT COUNT(*) or water_dataframe_describe to learn the true match count. Requires DataCanvas to be enabled on this server instance. Returns an error if DataCanvas is not available.
  - after: Run a read-only SQL SELECT against water data tables staged on a DataCanvas by water_get_series or water_find_sites. Workflow: run water_get_series or water_find_sites (get canvas_id + table_name) → water_dataframe_describe (confirm the table and its columns) → water_dataframe_query (SQL analysis), then optionally water_dataframe_drop (when this server enables it) to remove a table you are done with. Only SELECT statements are permitted. At most 10,000 rows are returned; a query matching more is capped and the response sets truncated=true — scope with WHERE/LIMIT, and use SELECT COUNT(*) or water_dataframe_describe to learn the true match count. Requires DataCanvas to be enabled on this server instance. Returns an error if DataCanvas is not available.
- 2026-09-25 `water_get_conditions` changed (description, outputSchema)
  - before: Get a USGS site's current reading ranked against its full period-of-record daily-mean percentiles for the same calendar day — a "how unusual is this" percentileClass (record-high to record-low), not a flood-stage or drought determination (this tool fetches no authoritative thresholds). The reading is instantaneous but the percentiles are daily-mean, so the ranking is approximate (see historicalContext.comparisonBasis). When the record is too short to rank, returns the reading with historicalContext=null instead of an error. Use water_find_sites and water_list_parameters to resolve inputs.
  - after: Get a USGS site's current reading ranked against its full period-of-record daily-mean percentiles for the same calendar day — a "how unusual is this" percentileClass (record-high to record-low), not a flood-stage or drought determination (this tool fetches no authoritative thresholds). The reading is instantaneous but the percentiles are daily-mean, so the ranking is approximate (see historicalContext.comparisonBasis). When the record is too short to rank, returns the reading with historicalContext=null instead of an error. A reading NWIS reports as no data (a seasonal, discontinued, dry, or malfunctioning gage) returns an empty currentValue with the qualifiers naming why, and is not ranked. When the site measures the parameter with several sensors (methods), one reading is used and named by methodId/methodDescription — from the sensor its percentile series is described as when one is, otherwise the most recent across them; water_get_readings lists every method. Use water_find_sites and water_list_parameters to resolve inputs.
- 2026-09-25 `water_get_series` changed (description, inputSchema, outputSchema)
  - before: Get a daily or instantaneous time series for one USGS site and parameter over a date range, as time-ordered value records. Large sets (>500 records) return the most recent records inline with truncated=true — the last 500 without DataCanvas, and with DataCanvas enabled the complete series also spills to a canvas (canvas_id/table_name): inspect the staged table with water_dataframe_describe, then read the full series with water_dataframe_query. Use water_find_sites and water_list_parameters to resolve inputs.
  - after: Get a daily or instantaneous time series for one USGS site and parameter over a date range, as time-ordered value records. NWIS can hold several series for one query — one per daily statistic (mean, maximum, minimum) and one per sensor (method); this returns one, reporting its statCd and methodId, and lists the rest in otherSeries so any can be re-requested with the statCd and methodId inputs. By default it returns the daily mean when NWIS returns one with values, and the method with the most records. Large sets (>500 records) return the most recent records inline with truncated=true — the last 500 without DataCanvas, and with DataCanvas enabled the complete series also spills to a canvas (canvas_id/table_name): inspect the staged table with water_dataframe_describe, then read the full series with water_dataframe_query. Use water_find_sites and water_list_parameters to resolve inputs.
- 2026-09-25 `water_get_readings` changed (description, inputSchema, outputSchema)
  - before: Get the latest instantaneous (~15-min, real-time) values for up to 100 USGS sites in one call — per-site, per-parameter records with timestamp, value, unit, and provisional/approved qualifiers. Each series returns only its 10 most recent records (totalValues reports the true count; truncated=true if any were capped); use water_get_series for a full date-range series. Sites NWIS returns nothing for are listed in missingSites, not dropped silently. Use water_find_sites first to discover site numbers and available parameters.
  - after: Get the latest instantaneous (~15-min, real-time) values for up to 100 USGS sites in one call — per-site, per-parameter records with timestamp, value, unit, and provisional/approved qualifiers. Omitting parameterCd returns every parameter each site publishes. A site measuring one parameter with several sensors returns one series per method, each named by methodId and methodDescription. At most 100 series return per call — every site that returned data keeps at least one, and totalSeries reports how many NWIS returned; pass parameterCd or split the sites across calls to reach the rest. Each series returns only its 10 most recent records (totalValues reports the true count); truncated=true when either cap applied. Use water_get_series for a full date-range series. Sites NWIS returns nothing for are listed in missingSites, not dropped silently. Use water_find_sites first to discover site numbers and available parameters.

Full history: https://api.protogrid.dev/v1/servers/io.github.cyanheads%2Fusgs-water-mcp-server/changes

## Dependencies

- npm @cyanheads/usgs-water-mcp-server@0.4.0: unknown
- npm @cyanheads/usgs-water-mcp-server@0.4.0: unknown

Dependency graphs from deps.dev; advisories from OSV.dev, including the GitHub Advisory Database and the PyPI Advisory Database; all CC BY 4.0.

## Remotes

- https://usgs-water.caseyjhand.com/mcp (streamable-http, auth none, reachable true, uptime30d 1)

## Packages

- npm @cyanheads/usgs-water-mcp-server@0.4.0
- npm @cyanheads/usgs-water-mcp-server@0.4.0

## Tools (8)

- `water_dataframe_describe`: List tables and columns staged on a DataCanvas by water_get_series or water_find_sites. Call this after water_get_series or water_find_sites returns a canvas_id to discover the exact table name and column types before writing a query. Then pass the table name to water_dataframe_query, or to water_dataframe_drop (when this server enables it) to remove a table you no longer need. Requires DataCanvas to be enabled on this server instance. Returns an error if DataCanvas is not available.
- `water_dataframe_query`: Run a read-only SQL SELECT against water data tables staged on a DataCanvas by water_get_series or water_find_sites. Workflow: run water_get_series or water_find_sites (get canvas_id + table_name) → water_dataframe_describe (confirm the table and its columns) → water_dataframe_query (SQL analysis), then optionally water_dataframe_drop (when this server enables it) to remove a table you are done with. Only SELECT statements are permitted. At most 10,000 rows are returned; a query matching more is capped and the response sets truncated=true — scope with WHERE/LIMIT, and use SELECT COUNT(*) or water_dataframe_describe to learn the true match count. Requires DataCanvas to be enabled on this server instance. Returns an error if DataCanvas is not available.
- `water_find_sites`: Find USGS water monitoring sites by bounding box, state, county, or HUC watershed code, filtered by site type and parameter availability. Returns site numbers, names, coordinates, types, altitude, and (in expanded mode) drainage area. Call this first — water_get_readings, water_get_series, and water_get_conditions all require a site number. Supply exactly one major filter — bbox, stateCd, countyCd, or huc; siteType, parameterCd, and hasDataTypeCd only narrow within it and cannot stand alone. Page through matches with limit/offset (500 per page); truncated=true means matches remain after the returned window and upstreamTotal holds the full count. When the match set exceeds 500 and DataCanvas is enabled, the complete set also stages to a canvas (canvas_id/table_name) — inspect it with water_dataframe_describe, then retrieve it with water_dataframe_query.
- `water_get_conditions`: Get a USGS site's current reading ranked against its full period-of-record daily-mean percentiles for the same calendar day — a "how unusual is this" percentileClass (record-high to record-low), not a flood-stage or drought determination (this tool fetches no authoritative thresholds). The reading is instantaneous but the percentiles are daily-mean, so the ranking is approximate (see historicalContext.comparisonBasis). When the record is too short to rank, returns the reading with historicalContext=null instead of an error. A reading NWIS reports as no data (a seasonal, discontinued, dry, or malfunctioning gage) returns an empty currentValue with the qualifiers naming why, and is not ranked. When the site measures the parameter with several sensors (methods), one reading is used and named by methodId/methodDescription — from the sensor its percentile series is described as when one is, otherwise the most recent across them; water_get_readings lists every method. Use water_find_sites and water_list_parameters to resolve inputs.
- `water_get_readings`: Get the latest instantaneous (~15-min, real-time) values for up to 100 USGS sites in one call — per-site, per-parameter records with timestamp, value, unit, and provisional/approved qualifiers. Omitting parameterCd returns every parameter each site publishes. A site measuring one parameter with several sensors returns one series per method, each named by methodId and methodDescription. At most 100 series return per call — every site that returned data keeps at least one, and totalSeries reports how many NWIS returned; pass parameterCd or split the sites across calls to reach the rest. Each series returns only its 10 most recent records (totalValues reports the true count); truncated=true when either cap applied. Use water_get_series for a full date-range series. Sites NWIS returns nothing for are listed in missingSites, not dropped silently. Use water_find_sites first to discover site numbers and available parameters.
- `water_get_series`: Get a daily or instantaneous time series for one USGS site and parameter over a date range, as time-ordered value records. NWIS can hold several series for one query — one per daily statistic (mean, maximum, minimum) and one per sensor (method); this returns one, reporting its statCd and methodId, and lists the rest in otherSeries so any can be re-requested with the statCd and methodId inputs. By default it returns the daily mean when NWIS returns one with values, and the method with the most records. Large sets (>500 records) return the most recent records inline with truncated=true — the last 500 without DataCanvas, and with DataCanvas enabled the complete series also spills to a canvas (canvas_id/table_name): inspect the staged table with water_dataframe_describe, then read the full series with water_dataframe_query. Use water_find_sites and water_list_parameters to resolve inputs.
- `water_get_site_data_catalog`: Discover a USGS site’s parameters, sensors, statistics, and UTC record bounds before calling water_get_series. Find site numbers with water_find_sites. Each row is a distinct series; metadata bounds may contain gaps and lag observations. For water_get_series, take YYYY-MM-DD from beginDate/endDate, pass statCd only for daily rows, and obtain methodId from a water_get_series response; timeSeriesId is an OGC identifier, never an NWIS methodId. Rows with null seriesType are other computations and cannot be requested with water_get_series. Follow nextCursor with the same filters and limit until null.
- `water_list_parameters`: Look up USGS parameter codes — the 5-digit codes every other tool's parameterCd takes. With no query, lists a curated set of well-known codes with names, units, and thematic group, from a built-in table (no network call): 00060 = "Discharge" (ft³/s), 00065 = "Gage height" (ft), 00010 = "Temperature, water" (°C), 72019 = "Depth to water level" (ft), and others; filter it with group. For anything else — turbidity, nitrate, chlorophyll, suspended sediment, salinity — pass query to search the full USGS parameter-code catalog (~19,600 codes) by name and description, or pass a 5-digit code to look it up. A query returns at most 25 entries, matching curated codes first, with total counting every match.
