io.github.cyanheads/epa-mcp-server

Official registry Search and web

Search EPA compliance, TRI, Superfund, drinking-water, EJScreen, and AirNow data.

What can you do with io.github.cyanheads/epa-mcp-server?

  • 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
  • Search results carry the IDs the next tool needs: registryId, fipsCode, facilityId, and triFacilityIds
  • epa_echo_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

Install

Add this to your client's MCP configuration (Claude Desktop: claude_desktop_config.json; Cursor: .cursor/mcp.json; VS Code and others use the same "mcpServers" shape).

Remote (streamable-http)
Claude Code
claude mcp add --transport http epa-mcp-server 'https://epa.caseyjhand.com/mcp'
JSON configuration (Claude Desktop, Cursor, VS Code)
{
    "mcpServers": {
        "epa-mcp-server": {
            "type": "http",
            "url": "https://epa.caseyjhand.com/mcp"
        }
    }
}
NPM package @cyanheads/epa-mcp-server
Claude Code
claude mcp add epa-mcp-server -e 'AIRNOW_API_KEY=<AIRNOW_API_KEY>' -e 'EJSCREEN_API_BASE_URL=https://api.ejanalysis.com' -e 'MCP_LOG_LEVEL=info' -- npx -y @cyanheads/epa-mcp-server@0.7.0
JSON configuration (Claude Desktop, Cursor, VS Code)
{
    "mcpServers": {
        "epa-mcp-server": {
            "command": "npx",
            "args": [
                "-y",
                "@cyanheads/epa-mcp-server@0.7.0"
            ],
            "env": {
                "AIRNOW_API_KEY": "<AIRNOW_API_KEY>",
                "EJSCREEN_API_BASE_URL": "https://api.ejanalysis.com",
                "MCP_LOG_LEVEL": "info"
            }
        }
    }
}

Environment variables

  • AIRNOW_API_KEY Optional AirNow API key for real-time and forecast air quality data. When unset, the epa_airnow_get_conditions tool is not registered and the other tools run without it. Free registration at docs.airnowapi.org.
  • EJSCREEN_API_BASE_URL Optional override for the EJScreen (EJAM) API base URL used by epa_ejscreen_get_scores. Defaults to https://api.ejanalysis.com — the community-maintained EJScreen rehost by Public Environmental Data Partners.
  • MCP_LOG_LEVEL Sets the minimum log level for output (e.g., 'debug', 'info', 'warn').
NPM package @cyanheads/epa-mcp-server
Claude Code
claude mcp add epa-mcp-server -e 'MCP_TRANSPORT_TYPE=<MCP_TRANSPORT_TYPE>' -e 'AIRNOW_API_KEY=<AIRNOW_API_KEY>' -e 'EJSCREEN_API_BASE_URL=https://api.ejanalysis.com' -e 'MCP_HTTP_HOST=127.0.0.1' -e 'MCP_HTTP_PORT=3010' -e 'MCP_HTTP_ENDPOINT_PATH=/mcp' -e 'MCP_AUTH_MODE=none' -e 'MCP_LOG_LEVEL=info' -- npx -y @cyanheads/epa-mcp-server@0.7.0
JSON configuration (Claude Desktop, Cursor, VS Code)
{
    "mcpServers": {
        "epa-mcp-server": {
            "command": "npx",
            "args": [
                "-y",
                "@cyanheads/epa-mcp-server@0.7.0"
            ],
            "env": {
                "MCP_TRANSPORT_TYPE": "<MCP_TRANSPORT_TYPE>",
                "AIRNOW_API_KEY": "<AIRNOW_API_KEY>",
                "EJSCREEN_API_BASE_URL": "https://api.ejanalysis.com",
                "MCP_HTTP_HOST": "127.0.0.1",
                "MCP_HTTP_PORT": "3010",
                "MCP_HTTP_ENDPOINT_PATH": "/mcp",
                "MCP_AUTH_MODE": "none",
                "MCP_LOG_LEVEL": "info"
            }
        }
    }
}

Environment variables

  • MCP_TRANSPORT_TYPE Selects the HTTP transport.
  • AIRNOW_API_KEY Optional AirNow API key for real-time and forecast air quality data. When unset, the epa_airnow_get_conditions tool is not registered and the other tools run without it. Free registration at docs.airnowapi.org.
  • EJSCREEN_API_BASE_URL Optional override for the EJScreen (EJAM) API base URL used by epa_ejscreen_get_scores. Defaults to https://api.ejanalysis.com — the community-maintained EJScreen rehost by Public Environmental Data Partners.
  • MCP_HTTP_HOST The hostname for the HTTP server.
  • MCP_HTTP_PORT The port to run the HTTP server on.
  • MCP_HTTP_ENDPOINT_PATH The endpoint path for the MCP server.
  • MCP_AUTH_MODE Authentication mode to use: 'none', 'jwt', or 'oauth'.
  • MCP_LOG_LEVEL Sets the minimum log level for output (e.g., 'debug', 'info', 'warn').

Documentation

From the project's README on GitHub, refreshed weekly. Links open on GitHub.

Contents
  1. @cyanheads/epa-mcp-server
  2. Overview
  3. Capability reference
  4. Features
  5. Getting started
  6. Configuration
  7. Running the server
  8. Project structure
  9. Development guide
  10. Contributing
  11. License

@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

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_echo_search_facilitiesSearch EPA-regulated facilities by location, environmental program, or compliance status across CAA, CWA, RCRA, TRI, and SDWA
epa_echo_get_facilityFull compliance profile for one facility by EPA Registry ID — compliance status, inspections, enforcement actions, penalties, and TRI IDs
epa_echo_search_violationsSearch EPA civil and criminal enforcement cases by state, program, or date range
epa_airnow_get_conditionsCurrent AQI observations or the issued AQI forecast from AirNow
epa_tri_get_facility_releasesPer-chemical Toxic Release Inventory data for a single facility
epa_tri_search_releasesToxic Release Inventory records across facilities in a state or county
epa_superfund_search_sitesSearch Superfund (CERCLA/SEMS) sites by location or NPL listing status
epa_water_search_systemsSearch drinking water systems (SDWIS) by state or ZIP code
epa_ejscreen_get_scoresEJScreen 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_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.

Capability reference

epa_echo_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_echo_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_echo_get_facility tool
  • registry_id comes from epa_echo_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_tri_get_facility_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_echo_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_airnow_get_conditions 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_tri_get_facility_releases tool
  • facility_id is the TRI facilityId from epa_tri_search_releases or epa_echo_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_tri_search_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_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)
  • Dioxin records reported in grams are converted to pounds and carry reportedUnit: "grams"

epa_superfund_search_sites 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 page latitude/longitude candidates to exhaustion, apply exact distance, and return the nearest sites first. A 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 out

epa_water_search_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_ejscreen_get_scores 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_echo_search_facilities
  • Returns the epa_echo_get_facility compliance profile; errors when no facility resolves

epa://superfund/{site_id} resource
  • site_id comes from epa_superfund_search_sites
  • 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_echo_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_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"
    }
  }
}
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_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
Prerequisites
  • Bun v1.4.0 or higher (or Node.js v24+); development and Docker use Bun v1.4.4.
  • (Optional) An AirNow API key to enable 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.
Installation
  1. Clone the repository:
git clone https://github.com/cyanheads/epa-mcp-server.git
  1. Navigate into the directory:
cd epa-mcp-server
  1. Install dependencies:
bun install
  1. 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:

VariableDescriptionDefault
AIRNOW_API_KEYOptional. Enables epa_airnow_get_conditions.—
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:
    # 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

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:

bun run devcheck
bun run test

License

Apache-2.0 — see LICENSE for details.

Frequently asked questions

What is io.github.cyanheads/epa-mcp-server?

Search EPA compliance, TRI, Superfund, drinking-water, EJScreen, and AirNow data.

How do I install io.github.cyanheads/epa-mcp-server?

It runs as a remote server. In Claude Code: claude mcp add --transport http epa-mcp-server 'https://epa.caseyjhand.com/mcp'. For other clients, paste the JSON configuration from this page into their MCP settings.

Does io.github.cyanheads/epa-mcp-server need an API key?

Its registry entry lists no required key or secret. Some features may still need an account with the service it connects to.

Is io.github.cyanheads/epa-mcp-server open source?

Yes. Its code is on GitHub under the Apache-2.0 license.

Who maintains io.github.cyanheads/epa-mcp-server?

It is published by cyanheads. AI.tn lists it but does not maintain or endorse it.

From the official MCP Registry. Listing is not an endorsement: review a server's code and permissions before giving it access to your data.