usgs-water-mcp-server

v0.5.0 pre-1.0

Query real-time and historical water data from ~8,000 USGS stream gages and groundwater wells via MCP. STDIO or Streamable HTTP.

usgs-water.caseyjhand.com/mcp
claude mcp add --transport http usgs-water-mcp-server https://usgs-water.caseyjhand.com/mcp
codex mcp add usgs-water-mcp-server --url https://usgs-water.caseyjhand.com/mcp
{
  "mcpServers": {
    "usgs-water-mcp-server": {
      "url": "https://usgs-water.caseyjhand.com/mcp"
    }
  }
}
gemini mcp add --transport http usgs-water-mcp-server https://usgs-water.caseyjhand.com/mcp
{
  "mcpServers": {
    "usgs-water-mcp-server": {
      "command": "bunx",
      "args": [
        "mcp-remote",
        "https://usgs-water.caseyjhand.com/mcp"
      ]
    }
  }
}
{
  "mcpServers": {
    "usgs-water-mcp-server": {
      "type": "http",
      "url": "https://usgs-water.caseyjhand.com/mcp"
    }
  }
}
curl -X POST https://usgs-water.caseyjhand.com/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'

Tools

9

read 8

water_list_parameters

open-world

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.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "water_list_parameters",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "group": {
      "default": "all",
      "description": "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.",
      "type": "string",
      "enum": [
        "streamflow",
        "groundwater",
        "temperature",
        "meteorological",
        "water-quality",
        "all"
      ]
    },
    "query": {
      "description": "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.",
      "type": "string",
      "maxLength": 200
    }
  },
  "required": [
    "group"
  ],
  "additionalProperties": false
}
view source ↗

water_find_sites

open-world

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.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "water_find_sites",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "bbox": {
      "description": "Bounding box as \"west,south,east,north\" in decimal degrees (e.g. \"-77.5,38.5,-76.5,39.5\" for the DC metro area). One of the four major filters — bbox, stateCd, countyCd, and huc are mutually exclusive with each other, and exactly one must be supplied.",
      "type": "string",
      "pattern": "^-?\\d+(\\.\\d+)?(,-?\\d+(\\.\\d+)?){3}$"
    },
    "stateCd": {
      "description": "2-character US state abbreviation (e.g. \"VA\", \"WA\"). Returns all sites in the state for the given filters. Major filter — supply exactly one of bbox, stateCd, countyCd, huc.",
      "type": "string",
      "pattern": "^[A-Za-z]{2}$"
    },
    "countyCd": {
      "description": "FIPS county code(s) as bare 5-digit numbers — state and county digits concatenated, no separator (e.g. \"51013\" for Arlington, VA). Comma-separate up to 20 (e.g. \"51059,51061\"). The 5 digits already encode the state, so the code stands alone. Major filter — supply exactly one of bbox, stateCd, countyCd, huc.",
      "type": "string",
      "pattern": "^\\d{5}(,\\d{5}){0,19}$"
    },
    "huc": {
      "description": "Hydrologic Unit Code (HUC) scoping results to a watershed. Either a 2-digit major HUC (e.g. \"02\" for the Mid-Atlantic region) or an 8-digit minor HUC (e.g. \"02070008\" for the Middle Potomac). NWIS accepts no other lengths. Major filter — supply exactly one of bbox, stateCd, countyCd, huc.",
      "type": "string",
      "pattern": "^(\\d{2}|\\d{8})$"
    },
    "siteType": {
      "description": "Site type filter. Common codes: \"ST\" (stream), \"GW\" (groundwater well), \"LK\" (lake/reservoir), \"SP\" (spring), \"AT\" (atmosphere), \"OC\" (ocean), \"ES\" (estuary). Comma-separate multiple types (e.g. \"ST,GW\").",
      "type": "string"
    },
    "parameterCd": {
      "description": "5-digit parameter code to require at each returned site (e.g. \"00060\" for discharge). Use water_list_parameters to discover codes. Comma-separate multiple codes with no spaces (e.g. \"00060,00065\").",
      "type": "string",
      "pattern": "^\\d{5}(,\\d{5})*$"
    },
    "hasDataTypeCd": {
      "description": "Require sites with data of this type. Common values: \"iv\" (real-time/instantaneous), \"dv\" (daily values), \"gw\" (groundwater). Comma-separate multiple types.",
      "type": "string"
    },
    "siteOutput": {
      "default": "basic",
      "description": "\"basic\" returns core identification fields. \"expanded\" adds drainage area, altitude, contributing area, and other metadata.",
      "type": "string",
      "enum": [
        "basic",
        "expanded"
      ]
    },
    "limit": {
      "default": 500,
      "description": "Maximum sites to return inline, 1–500. Default 500 (the inline cap).",
      "type": "integer",
      "minimum": 1,
      "maximum": 500
    },
    "offset": {
      "default": 0,
      "description": "Number of matching sites to skip before returning results. Page through matches beyond the inline cap by advancing offset by limit. Default 0.",
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    },
    "canvas_id": {
      "description": "Canvas ID from a prior call to add this match set as a table on an existing canvas rather than creating a new one. Each distinct filter set gets its own table name, so re-running the identical query replaces its own table while a different query adds another alongside it. Applies only when the match set exceeds the inline cap and DataCanvas is enabled. Omit to start a fresh canvas.",
      "type": "string",
      "pattern": "^[A-Za-z0-9_-]{10}$"
    }
  },
  "required": [
    "siteOutput",
    "limit",
    "offset"
  ],
  "additionalProperties": false
}
view source ↗

water_get_readings

open-world

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.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "water_get_readings",
    "arguments": {
      "sites": "<sites>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "sites": {
      "minItems": 1,
      "maxItems": 100,
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^\\d{8,15}$",
        "description": "A USGS site number (8–15 digits, e.g. \"01646500\")."
      },
      "description": "One or more USGS site numbers to query. Maximum 100 per call."
    },
    "parameterCd": {
      "description": "Parameter codes to return. Omit to get every parameter each site publishes — one series per site, parameter, and method, so a large batch can exceed the 100-series cap. Use water_list_parameters to discover codes.",
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^\\d{5}$",
        "description": "A 5-digit USGS parameter code (e.g. \"00060\" for discharge)."
      }
    },
    "period": {
      "default": "PT2H",
      "description": "ISO 8601 duration for the lookback period (e.g. \"PT2H\" = last 2 hours, \"P1D\" = last 1 day, \"P7D\" = last 7 days). Default: \"PT2H\" (last 2 hours of readings). Widening it raises totalValues, but each series still returns only its 10 most recent records — use water_get_series to retrieve a full series.",
      "type": "string",
      "pattern": "^P(?!$)(\\d+Y)?(\\d+M)?(\\d+W)?(\\d+D)?(T(?=\\d)(\\d+H)?(\\d+M)?(\\d+(\\.\\d+)?S)?)?$"
    }
  },
  "required": [
    "sites",
    "period"
  ],
  "additionalProperties": false
}
view source ↗

water_get_series

open-world

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.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "water_get_series",
    "arguments": {
      "site": "<site>",
      "parameterCd": "<parameterCd>",
      "startDate": "<startDate>",
      "endDate": "<endDate>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "site": {
      "type": "string",
      "pattern": "^\\d{8,15}$",
      "description": "USGS site number (8–15 digits, e.g. \"01646500\" for Potomac River at Little Falls). Use water_find_sites to discover valid site numbers."
    },
    "parameterCd": {
      "type": "string",
      "pattern": "^\\d{5}$",
      "description": "A single 5-digit USGS parameter code (e.g. \"00060\" for discharge, \"00065\" for gage height). One code per call — this tool returns one series. Use water_list_parameters to discover available codes."
    },
    "startDate": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
      "description": "Start date in YYYY-MM-DD format (e.g. \"2024-01-01\")."
    },
    "endDate": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
      "description": "End date in YYYY-MM-DD format (e.g. \"2024-12-31\")."
    },
    "seriesType": {
      "default": "daily",
      "description": "\"daily\" returns one value per day (DV service, typically mean/max/min). \"instantaneous\" returns ~15-minute readings (IV service). Default: \"daily\". Use \"instantaneous\" for high-resolution analysis.",
      "type": "string",
      "enum": [
        "daily",
        "instantaneous"
      ]
    },
    "statCd": {
      "description": "Daily statistic to return, as a 5-digit NWIS code: \"00003\" mean, \"00001\" maximum, \"00002\" minimum. Omit for the mean when NWIS returns one with values, otherwise the first statistic that has values; the response names the one returned and lists the others in otherSeries. Daily series only — instantaneous values carry just \"00000\".",
      "type": "string"
    },
    "methodId": {
      "description": "NWIS method ID of the sensor series to return — the methodId or an otherSeries[].methodId from a prior response for the same site, parameter, and seriesType. Omit for the method with the most records. IDs differ between daily and instantaneous series and between daily statistics.",
      "type": "string"
    },
    "canvas_id": {
      "description": "Canvas ID from a prior call to add this series as a table on an existing canvas rather than creating a new one. Each distinct site, parameter code, series type, date range, and selected statistic and method gets its own table name, so re-running the identical query replaces its own table while a different query adds another alongside it. Applies only when the series spills to a canvas. Omit to start a fresh canvas.",
      "type": "string",
      "pattern": "^[A-Za-z0-9_-]{10}$"
    }
  },
  "required": [
    "site",
    "parameterCd",
    "startDate",
    "endDate",
    "seriesType"
  ],
  "additionalProperties": false
}
view source ↗

water_get_site_data_catalog

open-world

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.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "water_get_site_data_catalog",
    "arguments": {
      "site": "<site>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "site": {
      "type": "string",
      "pattern": "^\\d{8,15}$",
      "description": "USGS site number (8–15 digits), from water_find_sites; e.g. \"01646500\"."
    },
    "parameterCd": {
      "description": "Optional 5-digit parameter code from water_list_parameters, e.g. \"00060\".",
      "type": "string",
      "pattern": "^\\d{5}$"
    },
    "seriesType": {
      "description": "Limit to Daily computations or Points/Instantaneous computations. Omit to include all periods.",
      "type": "string",
      "enum": [
        "daily",
        "instantaneous"
      ]
    },
    "limit": {
      "default": 100,
      "description": "Maximum metadata rows per page (1–1000; default 100). Keep unchanged when using cursor.",
      "type": "integer",
      "minimum": 1,
      "maximum": 1000
    },
    "cursor": {
      "description": "Opaque nextCursor from the previous page; keep site, parameterCd, seriesType, and limit unchanged.",
      "type": "string",
      "maxLength": 10000
    }
  },
  "required": [
    "site",
    "limit"
  ],
  "additionalProperties": false
}
view source ↗

water_get_conditions

open-world

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.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "water_get_conditions",
    "arguments": {
      "site": "<site>",
      "parameterCd": "<parameterCd>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "site": {
      "type": "string",
      "pattern": "^\\d{8,15}$",
      "description": "USGS site number (8–15 digits, e.g. \"01646500\" for Potomac River at Little Falls). Use water_find_sites to discover valid site numbers."
    },
    "parameterCd": {
      "type": "string",
      "pattern": "^\\d{5}$",
      "description": "5-digit USGS parameter code (e.g. \"00060\" for discharge, \"00065\" for gage height). Use water_list_parameters to discover codes."
    }
  },
  "required": [
    "site",
    "parameterCd"
  ],
  "additionalProperties": false
}
view source ↗

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.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "water_dataframe_query",
    "arguments": {
      "canvas_id": "<canvas_id>",
      "sql": "<sql>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "canvas_id": {
      "type": "string",
      "pattern": "^[A-Za-z0-9_-]{10}$",
      "description": "Canvas ID returned by water_get_series or water_find_sites. Identifies the canvas holding the data."
    },
    "sql": {
      "type": "string",
      "description": "Read-only SELECT statement. Reference the table by the table_name from water_get_series or water_find_sites; columns vary by source table, so run water_dataframe_describe first for the exact schema. Example: SELECT date_time, value FROM water_series_01646500_00060 ORDER BY date_time DESC LIMIT 10"
    }
  },
  "required": [
    "canvas_id",
    "sql"
  ],
  "additionalProperties": false
}
view source ↗

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.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "water_dataframe_describe",
    "arguments": {
      "canvas_id": "<canvas_id>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "canvas_id": {
      "type": "string",
      "pattern": "^[A-Za-z0-9_-]{10}$",
      "description": "Canvas ID returned by water_get_series or water_find_sites. Identifies the canvas to describe."
    }
  },
  "required": [
    "canvas_id"
  ],
  "additionalProperties": false
}
view source ↗

disabled 1

water_dataframe_drop

Drop one table from a DataCanvas — a table staged by water_get_series or water_find_sites — leaving the canvas and its other tables in place. Copy the table name from water_dataframe_describe. Returns the number of rows the dropped table held and the names of the tables still staged. A name that is not staged on the canvas fails and changes nothing, so repeating a drop is safe. Requires DataCanvas to be enabled on this server instance. Returns an error if DataCanvas is not available.

disabledwould be destructive

Disabled. Dropping staged tables is turned off in this deployment.

WATER_DATAFRAME_DROP_ENABLED=true
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "canvas_id": {
      "type": "string",
      "pattern": "^[A-Za-z0-9_-]{10}$",
      "description": "Canvas ID returned by water_get_series or water_find_sites. Identifies the canvas holding the table to drop."
    },
    "table_name": {
      "type": "string",
      "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,62}$",
      "description": "Name of the staged table to drop, as water_dataframe_describe lists it — the table_name water_get_series or water_find_sites returned. Matched case-insensitively. Only this table is removed."
    }
  },
  "required": [
    "canvas_id",
    "table_name"
  ],
  "additionalProperties": false
}
view source ↗

Resources

2

Site metadata for a USGS monitoring site: name, coordinates, type, HUC watershed code, state, county, drainage area, and altitude. Use water_find_sites to discover site numbers.

uri usgs-water://site/{siteId} mime application/json

Curated table of well-known USGS parameter codes — the codes most requests use (discharge, gage height, water temperature, groundwater levels, common water-quality measures), with human-readable names, units, and thematic domain. Injectable context for clients that support resources; the same list water_list_parameters returns without a query. A curated subset of the ~19,600-code USGS catalog — search the rest with water_list_parameters query.

uri usgs-water://parameters mime application/json

Extensions

1

io.modelcontextprotocol/skills

{
  "directoryRead": true
}