About usgs-water-mcp-server
usgs-water-mcp-server provides real-time and historical USGS water data for hydrology researchers and resource managers. It finds monitoring sites by location, retrieves readings and time series, and can stage large results for SQL queries.
Key features
- 01Find monitoring sites by geographic area
- 02Retrieve the latest site readings
- 03Query daily or instantaneous time series
- 04Rank current conditions against historical percentiles
- 05Stage data and run read-only SQL
Use cases
- 01Locate monitoring sites in a state, county, or watershed
- 02Track streamflow or groundwater-level changes
- 03Compare current readings with historical percentiles
MCP support
As an MCP server, it exposes USGS site, parameter, and water-data queries to clients through tools and resources. It supports STDIO and Streamable HTTP; large results can be staged in a DuckDB data canvas and queried with read-only SQL.
Configuration
Remote servers need no local install. Pick your client, then copy the config or install in one click.
https://usgs-water.caseyjhand.com/mcpclaude mcp add --transport http cyanheads-usgs-water https://usgs-water.caseyjhand.com/mcp{"mcpServers":{"cyanheads-usgs-water":{"type":"http","url":"https://usgs-water.caseyjhand.com/mcp"}}}Open in Cursor{"servers":{"cyanheads-usgs-water":{"type":"http","url":"https://usgs-water.caseyjhand.com/mcp"}}}Open in VS Code{"mcpServers":{"cyanheads-usgs-water":{"type":"http","url":"https://usgs-water.caseyjhand.com/mcp"}}}{"mcpServers":{"cyanheads-usgs-water":{"serverUrl":"https://usgs-water.caseyjhand.com/mcp"}}}Run commands in a terminal; paste JSON into the client’s MCP config and restart. Servers that need sign-in will prompt on first connect.
Tools
7| Parameter | Type | Required | Description |
|---|---|---|---|
| group | enum | No | Filter the curated list by thematic domain: "streamflow", "groundwater", "temperature", "meteorological", "water-quality", or "all" (default). Applies to the curated list only — leave it "all" when passing query.streamflow | groundwater | temperature | meteorological | water-quality | all |
| query | string | No | Search the full USGS parameter-code catalog. Case-insensitive; every word must start a word in the parameter name or description (e.g. "turbidity", "nitrate filtered", "dissolved oxygen"). A bare 5-digit code (e.g. "63680") returns that code. Omit to list the curated codes. |
Details
From the project README@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
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.
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
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"
]
}
}
}
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
Prerequisites
- Bun v1.4.0 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 | 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.
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/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.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.