Epa MCP Server README

The full upstream README, mirrored for reference. Install config, tool schemas, adoption signals and our own overview live on the Epa MCP Server listing page.

@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.

9 Tools • 2 Resources

Version License Docker MCP SDK npm TypeScript Bun

Install in Claude Desktop Install in Cursor Install in VS Code

Framework

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

ToolDescription
epa_search_facilitiesSearch EPA-regulated facilities by location, environmental program, or compliance status across CAA, CWA, RCRA, TRI, and SDWA
epa_get_facilityFull compliance profile for one facility by EPA Registry ID — compliance status, inspections, enforcement actions, penalties, and TRI IDs
epa_search_violationsSearch EPA civil and criminal enforcement cases by state, program, or date range
epa_get_air_qualityCurrent AQI observations or the issued AQI forecast from AirNow
epa_get_tri_releasesPer-chemical Toxic Release Inventory data for a single facility
epa_search_tri_releasesToxic Release Inventory records across facilities in a state or county
epa_search_superfundSearch Superfund (CERCLA/SEMS) sites by location or NPL listing status
epa_search_water_systemsSearch drinking water systems (SDWIS) by state or ZIP code
epa_get_ejscreenEJScreen environmental-justice indicators for a point and buffer

Resources

ResourceDescription
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 registryId for epa_get_facility, county and fipsCode, coordinates, per-program flags, and five-year inspection and penalty totals
  • programs takes one of CAA, CWA, RCRA, TRI, or SDWA; has_violation keeps facilities in significant noncompliance; active_only keeps facilities with an active permit

epa_get_facility tool

  • registry_id comes from epa_search_facilities
  • Returns complianceStatus, 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_get_tri_releases
  • If the status lookup fails, the profile returns without complianceStatus and a notice says so; an unknown ID fails as facility_not_found

epa_search_violations tool

  • Requires state 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 rejected

epa_get_air_quality tool

  • Provide zip_code or both latitude and longitude; mode is 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, and discussion, and forecast_date (YYYY-MM-DD) keeps one day
  • Readings carry categoryName and categoryNumber (1 Good–6 Hazardous); attribution marks the data preliminary and unsuitable for regulatory or enforcement decisions
  • Registered only when AIRNOW_API_KEY is set; responses cache for about one hour

epa_get_tri_releases tool

  • facility_id is the TRI facilityId from epa_search_tri_releases or epa_get_facility triFacilityIds; optional year (1987–2030) and partial chemical_name
  • Returns per-chemical air, water, land, and underground-injection releases plus a one-time/non-routine total, newest reporting year first when year is omitted. At 500 filings, both response surfaces warn that more may exist; narrow with year or chemical_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; optional county (partial match, case-insensitive), partial chemical_name, and year; up to 200 records (default 50), newest reporting year first when year is omitted
  • Returns facilityId for epa_get_tri_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)
  • Dioxin records reported in grams are converted to pounds and carry reportedUnit: "grams"

epa_search_superfund tool

  • Provide state/city/zip_code, or latitude + longitude + radius_miles (0.1–500); up to 200 sites (default 50)
  • Returns site identifiers, location, NPL, and cleanup status; npl_status filters listed, not-listed, proposed, or all (default)
  • Proximity searches check up to 500 candidate sites, then return those within the requested radius. A notice warns 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 state or zip_code; up to 200 systems (default 50). zip_code matches each system's address of record, not its service area
  • has_violation keeps systems with an open SDWIS violation; pws_type is community, non-transient, or transient, returned as CWS / NTNCWS / TNCWS in type
  • has_violation reads at most 1,000 records; when that fills, a notice says the list may miss violating systems

epa_get_ejscreen tool

  • Requires latitude and longitude; distance defaults to 1, unit to 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: false carries a note
  • EJScreen v2.2 (2022) comes from the community-maintained EJAM API (Public Environmental Data Partners)

epa://facility/{registry_id} resource

  • registry_id comes from epa_search_facilities
  • Returns the epa_get_facility compliance profile; errors when no facility resolves

epa://superfund/{site_id} resource

  • site_id comes from epa_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, and triFacilityIds
  • epa_get_facility returns a partial profile with a notice when the status lookup fails
  • Empty searches return a message that 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:

config.json
{
  "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.

config.json
{
  "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):

config.json
{
  "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:

config.json
{
  "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:

sh
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

  1. Clone the repository:
sh
git clone https://github.com/cyanheads/epa-mcp-server.git
  1. Navigate into the directory:
sh
cd epa-mcp-server
  1. Install dependencies:
sh
bun install
  1. Configure environment:
sh
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:

VariableDescriptionDefault
AIRNOW_API_KEYOptional. Enables epa_get_air_quality.—
EPA_ECHO_BASE_URLECHO API base URLhttps://echodata.epa.gov/echo
EPA_DMAP_BASE_URLEnvirofacts DMAP API base URLhttps://data.epa.gov/dmapservice
EPA_AIRNOW_BASE_URLAirNow API base URLhttps://www.airnowapi.org/aq
EJSCREEN_API_BASE_URLEJScreen (EJAM) API base URLhttps://api.ejanalysis.com
MCP_TRANSPORT_TYPETransport: stdio or httpstdio
MCP_HTTP_PORTHTTP server port3010
MCP_HTTP_ENDPOINT_PATHHTTP endpoint path/mcp
MCP_SESSION_MODEHTTP sessions: auto, stateful, or statelessstateless
MCP_AUTH_MODEAuth mode: none, jwt, or oauthnone
MCP_LOG_LEVELLog level (RFC 5424)info
LOG_TOOL_FAILURE_PAYLOADSLog failed calls' arguments and results with key-name redaction. Secrets inside free-form values remain visible.false
LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTESByte cap per logged payload16384
LOGS_DIRDirectory for log files (Node.js only)<project-root>/logs
STORAGE_PROVIDER_TYPEStorage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1in-memory
OTEL_ENABLEDEnable OpenTelemetry tracing and metricsfalse
OTEL_EXPORTER_OTLP_ENDPOINTOTLP base URL—
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT / OTEL_EXPORTER_OTLP_METRICS_ENDPOINTPer-signal endpoint overrides—
OTEL_EXPORTER_OTLP_LOGS_ENDPOINTEnables OTLP log export—

See .env.example for the full list of optional overrides.

Running the server

Local development

  • Build and run:

    sh
    # One-time build
    bun run rebuild
    
    # Run the built server
    bun run start:stdio
    # or
    bun run start:http
    
  • Run checks and tests:

    sh
    bun run devcheck   # Lint, format, typecheck, security
    bun run test       # Vitest test suite
    bun run lint:mcp   # Validate MCP definitions against spec
    

Docker

Terminal
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

DirectoryPurpose
src/index.tscreateApp() entry point — registers tools/resources and inits services.
src/configServer-specific environment variable parsing and validation with Zod.
src/mcp-server/toolsTool definitions (*.tool.ts). Nine tools across ECHO, DMAP, EJAM, and AirNow.
src/mcp-server/resourcesResource definitions (*.resource.ts). Facility and Superfund URI handlers.
src/services/echoECHO REST API service layer — facility search, facility detail, enforcement cases.
src/services/dmapEnvirofacts DMAP service layer — TRI releases, Superfund sites, drinking water systems.
src/services/airnowAirNow service layer — current and forecast AQI observations.
src/services/ejscreenEJScreen (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/catch in tool logic
  • Use ctx.log for request-scoped logging, ctx.state for 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:

sh
bun run devcheck
bun run test

License

Apache-2.0 — see LICENSE for details.