Search EPA drinking water systems, violations, enforcement, and contaminant measurements.
Runs on your machine from its package; the steps are below.
At most: only reads or looks up information.
Odel charges nothing per call. The publisher may charge for its own service.
Runs locally over stdio
This server isn't hosted — your MCP client launches it from a package registry. Use one of the commands below, or drop the config into your client (e.g. Claude Desktop).
Run
npx -y @cyanheads/epa-drinking-water-mcp-server
MCP client config
{
"mcpServers": {
"epa-drinking-water": {
"command": "npx",
"args": [
"-y",
"@cyanheads/epa-drinking-water-mcp-server"
]
}
}
}Run
npx -y @cyanheads/epa-drinking-water-mcp-server
MCP client config
{
"mcpServers": {
"epa-drinking-water": {
"command": "npx",
"args": [
"-y",
"@cyanheads/epa-drinking-water-mcp-server"
]
}
}
}Search EPA drinking water systems, violations, enforcement actions, and contaminant measurements via MCP. STDIO or Streamable HTTP.
EPA drinking-water records from SDWIS DMAP and the UCMR 5 publication. Resolve water systems, inspect historical violations and enforcement, and retrieve Lead and Copper Rule summaries or unregulated-contaminant occurrence measurements. Runs over stdio or Streamable HTTP without an API key.
| Tool | Description |
|---|---|
sdwis_list_reference | Decode identifiers, contaminant namespaces, coverage, standards, and EPA dictionaries |
sdwis_search_water_systems | Find contact-free public water-system inventory |
sdwis_get_system_profile | Retrieve summary, geography, service areas, or treatment |
sdwis_get_violations | Page reported historical SDWA violations |
sdwis_search_contaminants | Select violations, LCR summaries, or UCMR 5 occurrence |
sdwis_get_enforcement_actions | Retrieve actions or violation associations |
sdwis_dataframe_describe | Inspect tables and provenance inside a supplied canvas |
sdwis_dataframe_query | Run read-only SQL against staged partitions |
sdwis_dataframe_drop | Optionally remove one staged table |
The dataframe tools require npm or Docker with CANVAS_PROVIDER_TYPE=duckdb; drop also requires SDWIS_DATAFRAME_DROP_ENABLED=true. The Claude Desktop bundle provides the six EPA-reading tools.
sdwis_list_reference tooltopic: identifiers, system_types, sources, contaminants, violation_types, enforcement_types, treatment, service_areas, standards, or coverage; optional substring querysdwis_search_water_systems toolpwsid, name_contains, state, county (requires state), population_min / population_max, source, system_type, primacy_agency, or pwsid_prefixactivity defaults to active; choose inactive or all explicitly. State means reported legal-entity state; county means county served, and neither establishes complete physical service boundariessdwis_get_system_profile toolpwsid; section is summary (default), geography, service_areas, or treatment, independently pagedsystem; child sections return items. found:false reports missing inventory, not a compliance finding; operator contacts and mailing addresses are excludedsdwis_get_violations toolpwsid or state; optional county, contaminant, category, health_based, and compliance_statusdate_from / date_to select compliance-period overlap; a known endpoint can prove overlap despite a missing endpoint. Indeterminate periods are excluded with a count of affected rows examined during the call. Monitoring/reporting violations may lack concentrationscontaminant:lead selects 1030 and Lead and Copper Rule code 5000, including copper and monitoring records; either exact code selects only itselfstage:true requests a partition; stage_max_rows defaults to 5,000 and clamps at 25,000. Supply a held canvas_id to add another partitionsdwis_search_contaminants tooldataset (violations, lcr, or ucmr5) and pwsid or state; optional county, contaminant, and inclusive dates select compliance periods, LCR sample periods, or UCMR collection dates respectivelyPFAS covers 29 analytes; lithium is separate. zip and detected_only are UCMR-only; service ZIP is not sample location, and censored values remain unknown below reporting limitsstage, canvas_id, and stage_max_rows (5,000 default, 25,000 maximum). Cold UCMR calls may need a 180-second client timeout; broad state/county scopes resolve reported-state membership through resumable pages before returning occurrence rowssdwis_get_enforcement_actions toolpwsid; section is actions (default) or associations. Optional violation_id selects related actions through PWSID-bound association keysaction_type, originator, and inclusive enforcement-date filters apply only to actions. Associations page separately; source action text does not imply a federal order or monetary penaltystage, canvas_id, and stage_max_rows (5,000 default, 25,000 maximum)sdwis_dataframe_describe toolcanvas_id; optional exact table_name; limit defaults to 10 and clamps at 100 with cursor paginationcolumns array, row count, expiry, filters, provenance, completeness, and continuation; counts and limits are in tables. There is no global canvas listingrecord_cursor for lossless view:text chunks. Describe continuations expire after 15 minutes or table expiry, and reject changed metadataselection_coverage reports which source partitions and required companion relations remain staged, separately from SQL resultssdwis_dataframe_query toolcanvas_id and one read-only SELECT, WITH, or FROM statement in sql (at most 20,000 characters); describe the canvas firstCANVAS_DEFAULT_ROW_LIMIT (framework default 10,000), bounded by 25,000. row_count, truncated, and has_more distinguish retained rows from omitted rows; continuations read an immutable result for up to 15 minutessource_complete conservatively includes every live source partition, including tables the SQL does not referenceselection_coverage.chain_complete confirms the canvas holds a complete source retrieval chain; it does not establish which tables the SQL uses or override truncated. Query continuations retain the original coverage snapshotsdwis_dataframe_drop toolcanvas_id and exact table_name; requires CANVAS_PROVIDER_TYPE=duckdb and SDWIS_DATAFRAME_DROP_ENABLED=truedropped:false; other staged partitions remain availableBuilt 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.
EPA-specific:
Agent-friendly output:
effective_filters, source URL, retrieved_at, publication/HTTP validators when available, and interpretation caveats; source dates keep their calendar day, with non-midnight timestamps in a _raw fieldsource_complete:false can stay false with has_more:false once earlier rows went to other pages or partitions; aggregation does not restore itselection_partition identifies a retrieval and its delivered partition; canvas selection_coverage distinguishes a fully staged chain from missing, dropped, or expired partitions and companionslimit defaults to 10 and clamps at 100; the ~23 KB response budget can return fewer rows. Follow next_cursor with unchanged selectors, including empty phase:membership pages; cursors expire after 15 idle minutesitem_too_large supplies record_cursor for view:text JSON chunks. content[] and structuredContent carry the same valuesNo EPA account or API key is required. Add the server to Claude Code:
claude mcp add epa-drinking-water-mcp-server -- npx -y @cyanheads/epa-drinking-water-mcp-server
For other MCP clients, add this stdio configuration:
{
"mcpServers": {
"epa-drinking-water-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/epa-drinking-water-mcp-server"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio"
}
}
}
}
For SQL staging, add "CANVAS_PROVIDER_TYPE": "duckdb" to env (Docker: -e CANVAS_PROVIDER_TYPE=duckdb), call a source tool with stage:true, and pass the returned dataset.canvas_id to sdwis_dataframe_describe and sdwis_dataframe_query. The Claude Desktop bundle fixes CANVAS_PROVIDER_TYPE=none; staging there returns canvas_disabled.
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
data.epa.gov and www.epa.gov; allow a 180-second client timeout for the first UCMR call, which downloads and indexes the archive.git clone https://github.com/cyanheads/epa-drinking-water-mcp-server.git
cd epa-drinking-water-mcp-server
bun install
cp .env.example .env
# set CANVAS_PROVIDER_TYPE=duckdb to enable SQL staging
| Variable | Description | Default |
|---|---|---|
SDWIS_DATAFRAME_DROP_ENABLED | Enable early removal of staged tables when canvas is enabled | false |
CANVAS_PROVIDER_TYPE | none or duckdb; npm/Docker supports DuckDB, MCPB fixes none | none |
CANVAS_DEFAULT_ROW_LIMIT | SQL row ceiling, bounded by server maximum 25,000 | 10000 |
CANVAS_DEFAULT_MEMORY_LIMIT_MB | Per-engine budget, at most 512 MiB | 512 |
MCP_TRANSPORT_TYPE | stdio or http | stdio |
MCP_HTTP_HOST | HTTP bind address; Docker binds 0.0.0.0 | 127.0.0.1 |
MCP_HTTP_PORT | HTTP port | 3010 |
MCP_AUTH_MODE | none, jwt, or oauth | none |
MCP_LOG_LEVEL | Minimum log level | info |
OTEL_ENABLED | Enable OpenTelemetry | false |
See .env.example for the full overrides.
bun run rebuild
bun run start:stdio
# or bun run start:http
bun run devcheck
bun run test
docker build -t epa-drinking-water-mcp-server .
docker run --rm -p 3010:3010 epa-drinking-water-mcp-server
The image defaults to Streamable HTTP on port 3010 with stateless sessions. Add -e CANVAS_PROVIDER_TYPE=duckdb for SQL staging.
| Path | Purpose |
|---|---|
src/index.ts | Registration, source initialization, and optional canvas |
src/config | Dataframe-drop configuration |
src/domain | Selectors, reference data, schemas, and dual-surface formatting |
src/mcp-server/tools/definitions | Six EPA tools and three optional dataframe tools |
src/services/sdwis | Validated DMAP reads, membership, paging, and joins |
src/services/ucmr5 | Archive acquisition, parsing, indexing, and occurrence |
src/services/dataframe | Capability-scoped staging, SQL, expiry, and removal |
tests | Domain, service, tool, and integration tests |
scripts | Build, checks, packaging, and release tooling |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for logging; source results never go to ctx.state or user storagecreateApp() array in src/index.tsIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.