Search EPA compliance, TRI, Superfund, drinking-water, EJScreen, and AirNow data.
One address for any MCP client. No sign-in needed on this server.
9 read
Grouped by tool name. At most: only reads or looks up information.
Odel charges nothing per call. The publisher may charge for its own service.
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_echo_search_facilities | Search EPA-regulated facilities by location, environmental program, or compliance status across CAA, CWA, RCRA, TRI, and SDWA |
epa_echo_get_facility | Full compliance profile for one facility by EPA Registry ID — compliance status, inspections, enforcement actions, penalties, and TRI IDs |
epa_echo_search_violations | Search EPA civil and criminal enforcement cases by state, program, or date range |
epa_airnow_get_conditions | Current AQI observations or the issued AQI forecast from AirNow |
epa_tri_get_facility_releases | Per-chemical Toxic Release Inventory data for a single facility |
epa_tri_search_releases | Toxic Release Inventory records across facilities in a state or county |
epa_superfund_search_sites | Search Superfund (CERCLA/SEMS) sites by location or NPL listing status |
epa_water_search_systems | Search drinking water systems (SDWIS) by state or ZIP code |
epa_ejscreen_get_scores | 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_echo_get_facility) |
epa://superfund/{site_id} | Superfund site record by SEMS site ID |
All resource data is also reachable via tools — use epa_echo_get_facility and epa_superfund_search_sites for programmatic access in tool-only MCP clients.
epa_echo_search_facilities toolregistryId for epa_echo_get_facility, county and fipsCode, coordinates, per-program flags, and five-year inspection and penalty totalsprograms takes one of CAA, CWA, RCRA, TRI, or SDWA; has_violation keeps facilities in significant noncompliance; active_only keeps facilities with an active permitepa_echo_get_facility toolregistry_id comes from epa_echo_search_facilitiescomplianceStatus, a per-statute programCompliance summary, the newest 100 inspections from the last five years (inspectionsTotal counts them all), five years of enforcement actions with penalties, the latest TRI total with triReleasesTransfersYear, and triFacilityIds for epa_tri_get_facility_releasescomplianceStatus and a notice says so; an unknown ID fails as facility_not_foundepa_echo_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) bound the filing date. Only civil cases carry filing dates, so with a date all returns civil cases with a notice, and criminal is rejectedepa_airnow_get_conditions toolzip_code or both latitude and longitude; mode is current (default) or forecastdateValid, actionDay, and discussion, and forecast_date (YYYY-MM-DD) keeps one daycategoryName and categoryNumber (1 Good–6 Hazardous); attribution marks the data preliminary and unsuitable for regulatory or enforcement decisionsAIRNOW_API_KEY is set; responses cache for about one hourepa_tri_get_facility_releases toolfacility_id is the TRI facilityId from epa_tri_search_releases or epa_echo_get_facility triFacilityIds; optional year (1987–2030) and partial chemical_nameyear is omitted. At 500 filings, both response surfaces warn that more may exist; narrow with year or chemical_namereportedUnit: "grams"epa_tri_search_releases toolstate; optional county (partial match, case-insensitive), partial chemical_name, and year; up to 200 records (default 50), newest reporting year first when year is omittedfacilityId for epa_tri_get_facility_releases and each record's one-time/non-routine total; 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)reportedUnit: "grams"epa_superfund_search_sites toolstate/city/zip_code, or latitude + longitude + radius_miles (0.1–500); up to 200 sites (default 50)npl_status filters listed, not-listed, proposed, or all (default)notice says how many matching sites the result limit omitted; sites without coordinates cannot be located. All candidate pages share a 50-second deadline — narrow the radius or add filters if it times outepa_water_search_systems toolstate or zip_code; up to 200 systems (default 50). zip_code matches each system's address of record, not its service areahas_violation keeps systems with an open SDWIS violation; pws_type is community, non-transient, or transient, returned as CWS / NTNCWS / TNCWS in typehas_violation reads at most 1,000 records; when that fills, a notice says the list may miss violating systemsepa_ejscreen_get_scores 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_echo_search_facilitiesepa_echo_get_facility compliance profile; errors when no facility resolvesepa://superfund/{site_id} resourcesite_id comes from epa_superfund_search_sitesBuilt 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 over ECHO (facility compliance), Envirofacts DMAP (TRI, Superfund, SDWIS), AirNow (air quality), and the community-maintained EJAM API (EJScreen v2.2, 2022)Agent-friendly output:
registryId, fipsCode, facilityId, and triFacilityIdsepa_echo_get_facility returns a partial profile with a notice when the status lookup failsmessage that echoes the applied filters and suggests how to broaden themA public instance is available at https://epa.caseyjhand.com/mcp — no installation required. It runs without an AirNow key, so epa_airnow_get_conditions 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_airnow_get_conditions (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_airnow_get_conditions — 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_airnow_get_conditions. | — |
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.
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.