@cyanheads/epa-mcp-server
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
Overview
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.
Tools
| 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 |
Resources
| 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.
Capability reference
epa_search_facilities tool
- Provide ZIP code, state, city (prefix match, preferably with state), or latitude + longitude + radius_miles (max 100 miles); up to 100 results (default 50)
- Returns
registryIdforepa_get_facility, county andfipsCode, coordinates, per-program flags, and five-year inspection and penalty totals programstakes one of CAA, CWA, RCRA, TRI, or SDWA;has_violationkeeps facilities in significant noncompliance;active_onlykeeps facilities with an active permit
epa_get_facility tool
registry_idcomes fromepa_search_facilities- Returns
complianceStatus, a per-statuteprogramCompliancesummary, the newest 100 inspections from the last five years (inspectionsTotalcounts them all), five years of enforcement actions with penalties, the latest TRI total withtriReleasesTransfersYear, andtriFacilityIdsforepa_get_tri_releases - If the status lookup fails, the profile returns without
complianceStatusand anoticesays so; an unknown ID fails asfacility_not_found
epa_search_violations tool
- Requires
stateorzip_code; up to 100 cases (default 50) programfilters CAA, CWA, RCRA, SDWA, CERCLA, FIFRA, or TSCA, matching any law a case cites;case_typeis civil, criminal, or all (default)date_filed_start/date_filed_end(YYYY-MM-DD, inclusive) bound the filing date. Only civil cases carry filing dates, so with a dateallreturns civil cases with anotice, andcriminalis rejected
epa_get_air_quality tool
- Provide
zip_codeor bothlatitudeandlongitude;modeis current (default) or forecast - Current mode returns each pollutant's latest hourly NowCast AQI from the closest monitor within 50 miles; forecast mode returns each issued day with
dateValid,actionDay, anddiscussion, andforecast_date(YYYY-MM-DD) keeps one day - Readings carry
categoryNameandcategoryNumber(1 Good–6 Hazardous);attributionmarks the data preliminary and unsuitable for regulatory or enforcement decisions - Registered only when
AIRNOW_API_KEYis set; responses cache for about one hour
epa_get_tri_releases tool
facility_idis the TRIfacilityIdfromepa_search_tri_releasesorepa_get_facilitytriFacilityIds; optionalyear(1987–2030) and partialchemical_name- Returns per-chemical air, water, land, and underground-injection releases plus a one-time/non-routine total, newest reporting year first when
yearis omitted. At 500 filings, both response surfaces warn that more may exist; narrow withyearorchemical_name - Quantities are in pounds; dioxin records reported in grams are converted and carry
reportedUnit: "grams"
epa_search_tri_releases tool
- Requires 2-letter
state; optionalcounty(partial match, case-insensitive), partialchemical_name, andyear; up to 200 records (default 50), newest reporting year first whenyearis omitted - Returns
facilityIdforepa_get_tri_releasesand each record's one-time/non-routine total;include_release_breakdown: trueadds 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) - Dioxin records reported in grams are converted to pounds and carry
reportedUnit: "grams"
epa_search_superfund tool
- Provide
state/city/zip_code, orlatitude+longitude+radius_miles(0.1–500); up to 200 sites (default 50) - Returns site identifiers, location, NPL, and cleanup status;
npl_statusfilters listed, not-listed, proposed, or all (default) - Proximity searches check up to 500 candidate sites, then return those within the requested radius. A
noticewarns when the candidate cap may have omitted nearby sites, even if none were returned; sites without coordinates cannot be located
epa_search_water_systems tool
- Requires
stateorzip_code; up to 200 systems (default 50).zip_codematches each system's address of record, not its service area has_violationkeeps systems with an open SDWIS violation;pws_typeis community, non-transient, or transient, returned as CWS / NTNCWS / TNCWS intypehas_violationreads at most 1,000 records; when that fills, anoticesays the list may miss violating systems
epa_get_ejscreen tool
- Requires
latitudeandlongitude;distancedefaults to 1,unitto miles (kilometers accepted); buffer capped at 15 miles - Returns 13 environmental and 6 demographic indicators with national/state percentiles and demographic indices; outside coverage,
coverage.valid: falsecarries a note - EJScreen v2.2 (2022) comes from the community-maintained EJAM API (Public Environmental Data Partners)
epa://facility/{registry_id} resource
registry_idcomes fromepa_search_facilities- Returns the
epa_get_facilitycompliance profile; errors when no facility resolves
epa://superfund/{site_id} resource
site_idcomes fromepa_search_superfund- Returns the Superfund site record; errors when no SEMS record resolves
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.
EPA-specific:
- One
epa_tool surface over ECHO (facility compliance), Envirofacts DMAP (TRI, Superfund, SDWIS), AirNow (air quality), and the community-maintained EJAM API (EJScreen v2.2, 2022) - AirNow responses cache for about one hour to stay within per-key rate limits
- TRI reporting lags about 18 months; the latest reporting year is typically two years behind
Agent-friendly output:
- Search results carry the IDs the next tool needs:
registryId,fipsCode,facilityId, andtriFacilityIds epa_get_facilityreturns a partial profile with anoticewhen the status lookup fails- Empty searches return a
messagethat echoes the applied filters and suggests how to broaden them
Getting started
Public Hosted Instance
A 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"
}
}
}
Self-Hosted / Local
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
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+); development and Docker use Bun v1.4.3.
- (Optional) An AirNow API key to enable
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.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/epa-mcp-server.git
- Navigate into the directory:
cd epa-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# optionally set AIRNOW_API_KEY to enable the air quality tool
Configuration
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. | — |
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 | EJScreen (EJAM) API base URL | 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 | 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 | 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 | — |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT / OTEL_EXPORTER_OTLP_METRICS_ENDPOINT | Per-signal endpoint overrides | — |
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT | Enables OTLP log export | — |
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 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.
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). 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/. |
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
- ECHO searches must enforce at least one geographic parameter — unscoped queries time out against the live API
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
Apache-2.0 — see LICENSE for details.