
Wraps five EPA public data APIs into a single MCP interface: ECHO for facility compliance and enforcement actions, TRI for toxic chemical releases, AirNow for real-time air quality readings, CERCLA for Superfund site status, and SDWIS for drinking water violations. You get eight tools that let you search facilities by location or program, pull full compliance profiles with inspection history and penalties, query TRI releases by chemical and medium, check current AQI by ZIP or coordinates, and find Superfund sites or water systems with active violations. The facility search returns Registry IDs and county FIPS codes, which makes it easy to chain lookups or join with Census data. Useful for environmental compliance checks, public health analysis, or building location intelligence into regulatory workflows.
Search and retrieve EPA environmental data: facility compliance (ECHO), toxic releases (TRI), Superfund sites, drinking water systems, environmental-justice screening (EJScreen), and real-time air quality (AirNow). STDIO or Streamable HTTP.
Public Hosted Server: https://epa.caseyjhand.com/mcp
EPA environmental data across five federal programs — facility compliance (ECHO), toxic releases (TRI), Superfund sites, drinking water systems (SDWIS), and environmental-justice screening (EJScreen) — plus real-time AirNow air quality. Pull a facility's inspection and enforcement history, track toxic releases across a region, or screen any point for environmental-justice risk.
| Tool | Description |
|---|---|
epa_search_facilities | Search EPA-regulated facilities by location, environmental program, or compliance status across CAA, CWA, RCRA, TRI, and SDWA |
epa_get_facility | Full compliance profile for one facility by EPA Registry ID — compliance status, inspections, enforcement actions, penalties, and TRI IDs |
epa_search_violations | Search EPA civil and criminal enforcement cases by state, program, or date range |
epa_get_air_quality | Current AQI observations or the issued AQI forecast from AirNow |
epa_get_tri_releases | Per-chemical Toxic Release Inventory data for a single facility |
epa_search_tri_releases | Toxic Release Inventory records across facilities in a state or county |
epa_search_superfund | Search Superfund (CERCLA/SEMS) sites by location or NPL listing status |
epa_search_water_systems | Search drinking water systems (SDWIS) by state or ZIP code |
epa_get_ejscreen | EJScreen environmental-justice indicators for a point and buffer |
| Resource | Description |
|---|---|
epa://facility/{registry_id} | Full compliance profile for a facility by EPA Registry ID (same data as epa_get_facility) |
epa://superfund/{site_id} | Superfund site record by SEMS site ID |
All resource data is also reachable via tools — use epa_get_facility and epa_search_superfund for programmatic access in tool-only MCP clients.
epa_search_facilities toolregistryId for epa_get_facility, county and fipsCode for Census queries, coordinates, per-program flags, and five-year inspection and penalty totalsprograms takes one of CAA, CWA, RCRA, TRI, or SDWA (ECHO filters on a single program); has_violation keeps facilities with a program in significant noncompliance; active_only keeps facilities with an active permitepa_get_facility toolregistry_id comes from epa_search_facilitiescomplianceStatus, a per-statute programCompliance summary, the newest 100 inspections and other compliance monitoring records from the last five years (inspectionsTotal counts them all), five years of formal enforcement actions with penalties, and the latest TRI total with triReleasesTransfersYeartriFacilityIds lists the facility's TRI IDs, most recently reported first, for epa_get_tri_releases; county and fipsCode come from the facility's program recordscomplianceStatus and a notice says so; an unknown ID fails after one request as facility_not_foundairCompliance / waterCompliance appear only for programs ECHO tracks the facility inepa_search_violations toolstate or zip_code; up to 100 cases (default 50)program filters CAA, CWA, RCRA, SDWA, CERCLA, FIFRA, or TSCA, matching any law a case cites; case_type is civil, criminal, or all (default)date_filed_start / date_filed_end (YYYY-MM-DD, inclusive, either or both) bound the filing date. ECHO records filing dates for civil cases only: with a date, all returns civil cases and a notice says criminal cases were excluded, and criminal is rejectedfacilityName and registryId are unavailable from this endpoint; discover facilities with epa_search_facilitiesepa_get_air_quality toolzip_code or both latitude and longitude; mode is current (default) or forecastsiteName, siteID, and reportingAgencydateValid, actionDay, and the agency's discussion; optional forecast_date (YYYY-MM-DD) keeps one day and is rejected in current mode. Category-only forecasts carry no aqicategoryName and categoryNumber (1 Good–6 Hazardous); attribution credits the agencies and the U.S. EPA AirNow program and marks the data preliminary, unsuitable for regulatory, trend, or enforcement decisionsAIRNOW_API_KEY is set; responses cache for about one hourepa_get_tri_releases toolfacility_id is the TRI facilityId from epa_search_tri_releases or an entry of epa_get_facility triFacilityIds; optional year (1987–2030, all available by default) and partial chemical_name (punctuation such as 4,4'- or [g,h,i] and Greek letters match as written)year or chemical_namereportedUnit: "grams"epa_search_tri_releases toolstate; optional county (bare name, case-insensitive partial match, so Lake also matches LAKE OF THE WOODS — each record's countyName says which; a trailing County, Parish, or Borough is dropped), partial chemical_name, and year; up to 200 records (default 50). Without year, records come newest reporting year first, so the limit keeps the most recent filingscounty and chemical_name match punctuation and Greek letters as written (Prince George's, 1,3-Butadiene); EPA's service can't take those characters in a URL, so the server searches with a one-character wildcard in their place and keeps only exact matches. When that wildcard reads its three-page limit before limit exact matches turn up, a notice says more may exist (the same holds for epa_get_tri_releases chemical_name and epa_search_superfund city)include_release_breakdown: true adds air, water, land, and underground-injection releases at the cost of one more upstream request (up to ~20 seconds when EPA has not cached it)facilityId for epa_get_tri_releases; enrichment marks results truncated when the limit is reachedreportedUnit: "grams"epa_search_superfund toolstate/city/zip_code, or latitude + longitude + radius_miles (0.1–500); up to 200 sites (default 50). city matches punctuation as written (St. Louis, Coeur d'Alene)npl_status filters listed, not-listed, proposed, or all (default)epa_search_water_systems toolstate or zip_code; up to 200 systems (default 50). zip_code matches each system's address of record (often the owner's or operator's office), not its service areahas_violation keeps systems with an open SDWIS violation; violations returned to compliance or closed out when the system was deactivated don't count. pws_type is community, non-transient, or transient, returned as CWS / NTNCWS / TNCWS in typestate alone, has_violation reads the state's first 1,000 open violation records; with zip_code, it reads up to 1,000 systems in the ZIP and checks them by PWSID. When either read fills, an enrichment notice says the list may miss violating systemsepa_get_ejscreen toollatitude and longitude; distance defaults to 1, unit to miles (kilometers accepted); buffer capped at 15 milescoverage.valid: false carries a noteepa://facility/{registry_id} resourceregistry_id comes from epa_search_facilitiesepa_get_facility compliance profile, with a notice field when ECHO's facility status lookup failed; errors when no facility resolvesepa://superfund/{site_id} resourcesite_id comes from epa_search_superfundBuilt 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:
epa_ tool surface: ECHO (facility compliance), Envirofacts DMAP (TRI, Superfund, SDWIS), AirNow (real-time air quality), and the community-maintained EJAM API rehosting EJScreen data (v2.2, 2022)epa_get_facility builds the profile from ECHO's detailed facility report plus one best-effort status lookup — two upstream requestsAgent-friendly output:
registryId for compliance lookups and fipsCode when available for Census queries; TRI search supplies facilityId and the facility profile supplies triFacilityIds for release detailsepa_get_facility returns the rest of the profile when ECHO's facility status lookup fails, with a notice naming the missing fieldsmessage field that echoes the applied filters and suggests how to broaden the searchA public instance is available at https://epa.caseyjhand.com/mcp — no installation required. It runs without an AirNow key, so epa_get_air_quality is not available there; the other 8 tools are. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"epa-mcp-server": {
"type": "streamable-http",
"url": "https://epa.caseyjhand.com/mcp"
}
}
}
Add the following to your MCP client configuration file. An AirNow API key is optional — set AIRNOW_API_KEY to enable epa_get_air_quality (register free at docs.airnowapi.org); without it the server starts with the other 8 tools. ECHO, DMAP, and EJScreen tools work without authentication.
{
"mcpServers": {
"epa-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/epa-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"AIRNOW_API_KEY": "your-airnow-key"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"epa-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/epa-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"AIRNOW_API_KEY": "your-airnow-key"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"epa-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"-e", "AIRNOW_API_KEY=your-airnow-key",
"ghcr.io/cyanheads/epa-mcp-server:latest"
]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 AIRNOW_API_KEY=... bun run start:http
# Server listens at http://localhost:3010/mcp
epa_get_air_quality — register free at docs.airnowapi.org/account/request. Without it the server runs the other 8 tools. ECHO, DMAP, and EJScreen tools require no API key.git clone https://github.com/cyanheads/epa-mcp-server.git
cd epa-mcp-server
bun install
cp .env.example .env
# optionally set AIRNOW_API_KEY to enable the air quality tool
All configuration is validated at startup via Zod schemas in src/config/. Key environment variables:
| Variable | Description | Default |
|---|---|---|
AIRNOW_API_KEY | Optional. Enables epa_get_air_quality when set; the server runs the other 8 tools without it. Free registration at docs.airnowapi.org. | — |
EPA_ECHO_BASE_URL | ECHO API base URL | https://echodata.epa.gov/echo |
EPA_DMAP_BASE_URL | Envirofacts DMAP API base URL | https://data.epa.gov/dmapservice |
EPA_AIRNOW_BASE_URL | AirNow API base URL | https://www.airnowapi.org/aq |
EJSCREEN_API_BASE_URL | Optional. EJScreen (EJAM) API base URL used by epa_get_ejscreen. | https://api.ejanalysis.com |
MCP_TRANSPORT_TYPE | Transport: stdio or http | stdio |
MCP_HTTP_PORT | HTTP server port | 3010 |
MCP_HTTP_ENDPOINT_PATH | HTTP endpoint path | /mcp |
MCP_SESSION_MODE | HTTP sessions: auto, stateful, or stateless. Explicit environment values override the server default; auto resolves to stateful. Tenant-scoped caching is independent of sessions. | stateless |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth | none |
MCP_LOG_LEVEL | Log level (RFC 5424) | info |
LOG_TOOL_FAILURE_PAYLOADS | Log failed calls' arguments and results with key-name redaction. Secrets inside free-form values remain visible. | false |
LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES | UTF-8 byte cap per logged payload. | 16384 |
LOGS_DIR | Directory for log files (Node.js only) | <project-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1 | in-memory |
OTEL_ENABLED | Enable OpenTelemetry tracing and metrics | false |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP base URL; traces use /v1/traces, metrics /v1/metrics. | — |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT / OTEL_EXPORTER_OTLP_METRICS_ENDPOINT | Signal-specific endpoint overrides, used as-is. | — |
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT | Enables OTLP log export; the base endpoint alone does not enable it. | — |
See .env.example for the full list of 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 build -t epa-mcp-server .
docker run --rm -e AIRNOW_API_KEY=your-key -p 3010:3010 epa-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/epa-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
| 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). Nine tools across ECHO, DMAP, EJAM, and AirNow. |
src/mcp-server/resources | Resource definitions (*.resource.ts). Facility and Superfund URI handlers. |
src/services/echo | ECHO REST API service layer — facility search, facility detail, enforcement cases. |
src/services/dmap | Envirofacts DMAP service layer — TRI releases, Superfund sites, drinking water systems. |
src/services/airnow | AirNow service layer — current and forecast AQI observations. |
src/services/ejscreen | EJScreen (EJAM) service layer — environmental-justice indicators for a point + buffer. |
tests/ | Unit and integration tests mirroring src/. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped logging, ctx.state for tenant-scoped storagesrc/mcp-server/*/definitions/index.tsIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.
AIRNOW_API_KEY*AirNow API key for real-time and forecast air quality data. Free registration at docs.airnowapi.org.
MCP_LOG_LEVELdefault: infoSets the minimum log level for output (e.g., 'debug', 'info', 'warn').
MCP_HTTP_HOSTdefault: 127.0.0.1The hostname for the HTTP server.
MCP_HTTP_PORTdefault: 3010The port to run the HTTP server on.
MCP_HTTP_ENDPOINT_PATHdefault: /mcpThe endpoint path for the MCP server.
MCP_AUTH_MODEdefault: noneAuthentication mode to use: 'none', 'jwt', or 'oauth'.