npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cyanheads/epa-mcp-server

v0.8.0

Published

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.

Readme

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

| Tool | Description | |:---|:---| | epa_echo_search_facilities | Search EPA-regulated facilities by location, county, name, NAICS code, 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 or program ID — compliance status, inspections, enforcement actions, penalties, drinking-water violations, and TRI IDs | | epa_echo_search_violations | Search EPA civil and criminal enforcement cases by state, ZIP code, program, case type, or date range | | epa_echo_get_case | One civil enforcement case with its facilities, defendants, and settlements | | 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_superfund_get_site | Exact Superfund site record and all contaminant associations | | epa_water_search_systems | Search drinking water systems (SDWIS) by state, ZIP code, or county served | | epa_ejscreen_get_scores | 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 or program ID (the default pages of epa_echo_get_facility) | | epa://superfund/{site_id} | Superfund site record and contaminant associations by SEMS site ID |

All resource data is also reachable via tools — use epa_echo_get_facility and epa_superfund_get_site 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), county (prefix match on the recorded name; needs state), 5-digit county_fips, or latitude + longitude + radius_miles (max 100 miles). facility_name (whole words in any order, no commas; also matches other names ECHO records for the facility) and naics (2–6 digits, prefix match) narrow a geographic search
  • Up to 100 facilities per call (default 50); a larger limit, up to 5,000, returns 100 with a limitNotice. Page with offset and nextOffset; totalCount is the match before limit and offset
  • Returns registryId for epa_echo_get_facility, triFacilityIds for epa_tri_get_facility_releases, county and fipsCode, coordinates, per-program flags, and five-year inspection and penalty totals. effectiveQuery echoes the filters as sent
  • 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 is a Registry ID from epa_echo_search_facilities or a program ID ECHO resolves to one: a PWSID from epa_water_search_systems, a TRI facility ID, an NPDES permit, or a RCRA handler ID. The profile carries the resolved registryId
  • Returns complianceStatus, the naics codes its programs record, a per-statute programCompliance summary, five years of enforcement actions with penalties (caseId feeds epa_echo_get_case), the latest TRI total with triReleasesTransfersYear, and triFacilityIds for epa_tri_get_facility_releases
  • Two paged lists: inspections from the last five years, newest first (inspections_limit default and cap 100, inspections_offset; inspectionsTotal counts them all and inspectionsNextOffset continues), and drinkingWaterViolations for a public water system, newest compliance period first, each with contaminant, measured level, MCL, status, and enforcement actions (drinking_water_violations_limit default 50, cap 200, drinking_water_violations_offset; drinkingWaterViolationsTotal and drinkingWaterViolationsNextOffset). A larger limit returns the cap with a limitNotice, and truncated is true while either list has more
  • 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 per call (default 50), and a larger limit up to 5,000 returns 100 with a limitNotice. Page with offset and nextOffset; effectiveQuery echoes the filters as sent
  • program filters CAA, CWA, RCRA, SDWA, CERCLA, EPCRA, 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
  • Returns caseId, caseName, caseType, caseStatus, enforcementOutcome, leadAgency, the primary law (programsViolated) and primarySection, federal and state/local penalties, cost recovery, compliance action and SEP costs, and filed, settlement, and closed dates. Criminal cases omit status, outcome, state/local penalty, and cost fields
  • Case rows name no facility: pass a civil caseId to epa_echo_get_case

epa_echo_get_case tool

  • case_id is a civil case number: a caseId from epa_echo_search_violations (such as 03-2014-7010) or a formal action's caseId from epa_echo_get_facility. A criminal CR_ number fails as criminal_case before any request; an unknown number fails as case_not_found
  • Returns the case's category code, enforcement type, status, lead agency, relief sought, outcome, court and DOJ docket numbers, summary, and penalty, cost-recovery, compliance-action, and SEP totals, plus lawsAndSections, defendants, pollutants, and milestones
  • facilities lists the facilities the case names, one per Registry ID, with address, NAICS and SIC codes, and the program IDs the case links; pass a registryId to epa_echo_get_facility
  • enforcementConclusions lists each order, consent decree, or other settlement with its facilities, dates, penalties and costs, complying actions, pollutant reductions, and Supplemental Environmental Projects
  • The report is not paged: a case naming 115 facilities and 117 settlements returns about 120 KB across both response surfaces

epa_airnow_get_conditions tool

  • Provide zip_code (5 digits, or a ZIP+4 read as its first five) 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 (a real YYYY-MM-DD date) 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 on-site releases to air, water, land, and underground injection, their sum totalOnSiteReleasesInLbs, and the Form R Section 8.8 oneTimeReleaseInLbs, which the media quantities already include, newest reporting year first
  • limit defaults to and is capped at 500 filings; a larger value up to 5,000 returns 500 with a limitNotice. Page with offset and nextOffset; truncated, shown, cap, and guidance mark a page with more
  • Quantities are in pounds; dioxin records reported in grams are converted and carry reportedUnit: "grams"

epa_tri_search_releases tool

  • Requires 2-letter state (any case); optional county (case-insensitive partial match on the bare name; a trailing County, Parish, Borough, City and Borough, Census Area, or Municipality is dropped), partial chemical_name, and year; newest reporting year first when year is omitted
  • Up to 200 records per call (default 50); a larger limit up to 5,000 returns 200 with a limitNotice. Page with offset and nextOffset; effectiveQuery echoes the filters as sent. Records come in reporting-year order, not ranked by quantity
  • Returns facilityId for epa_tri_get_facility_releases, registryId for epa_echo_get_facility, parentCompany, and the Form R Section 8.8 oneTimeReleaseInLbs; include_release_breakdown: true adds on-site releases to air, water, land, and underground injection plus totalOnSiteReleasesInLbs, 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 (a ZIP+4 searches its 5-digit ZIP), or latitude + longitude + radius_miles (0.1–500); up to 200 sites per call (default 50), and a larger limit up to 5,000 returns 200 with a limitNotice. Page with offset and nextOffset; effectiveQuery echoes the filters as sent
  • Returns site identifiers, location, raw NPL code and source label, plus sparse non-NPL assessment, archive, EPA ID, and federal facility metadata. npl_status filters listed (F/A), not-listed (N), proposed (P), deleted (D), removed (R), withdrawn (W), or all (default)
  • exclude_archived: true requires SEMS archived_ind=N before pagination or proximity selection; the default retains all archive statuses. Assessment labels do not describe cleanup progress
  • Administrative searches return sites in SEMS site ID order and carry no totalCount, since DMAP reports no match count; follow nextOffset
  • Proximity searches rescan every latitude/longitude candidate on each call, apply exact distance, and page the nearest-first order (site ID breaks ties). totalCount is the number of matching sites, and a notice gives the page's place in it ("Showing sites a–b of N") and says 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_superfund_get_site tool

  • Provide a site_id from epa_superfund_search_sites, preserving leading zeros
  • Returns { site, contaminants }: site metadata plus every recorded contaminant association, including medium, operable unit, action, and type code when available. Repeated chemicals and duplicate source rows are retained
  • An existing site with no associations returns contaminants: []; a missing site returns NotFound. Site and paged contaminant reads share one 50-second deadline; upstream failures remain errors
  • Associations describe EPA action/media records, not current exposure, concentrations, or current risk

epa_water_search_systems tool

  • Requires state (a 2-letter code, or an EPA region code 01–10 for tribal systems) or zip_code; up to 200 systems per call (default 50) in PWSID order, and a larger limit up to 5,000 returns 200 with a limitNotice. Page with offset and nextOffset; totalCount appears when the call saw the whole match; effectiveQuery echoes the filters as sent
  • zip_code matches each system's address of record by prefix, so stored ZIP+4s match, and not its service area. county (needs state) finds the systems serving a county: a trailing County, Parish, Borough, City and Borough, Census Area, or Municipality is dropped, and a system matches only when one county it serves is that county, so Harris does not match Harrison
  • has_violation keeps systems with a current SDWA violation in ECHO, which applies EPA's Enforcement Response Policy and so flags fewer systems than SDWIS's unresolved violation records; pws_type is community, non-transient, or transient, returned as CWS / NTNCWS / TNCWS in type; active_only keeps systems SDWIS lists as active
  • A ZIP or county has_violation check reads up to 1,000 systems per call; when that fills before the page does, a notice says so and nextOffset continues
  • Pass a returned pwsid as registry_id to epa_echo_get_facility for the system's drinking-water violations

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, plus the demographicIndex and supplementalDemographicIndex, each with its usAverage. Both indexes are unitless, shifted means of z-scores, not percentages; an indicator's ejIndex is the Demographic Index × its US percentile, population-weighted over the buffer
  • coverage.valid: false with a note when the point is outside EJAM's bounds (the 50 states, DC, Puerto Rico, and the U.S. Island Areas; no request is made), the buffer holds no Census block (a larger distance may help below 15 miles), or its blocks have no residents (state, population, and block-group count are kept)
  • Data is EJAM's ACS 2018–2022 release of EJScreen (EJScreen 2.3 method), served by the community-maintained EJAM API (Public Environmental Data Partners)

epa://facility/{registry_id} resource

  • registry_id is a Registry ID from epa_echo_search_facilities or a program ID ECHO resolves to one, such as a PWSID from epa_water_search_systems
  • Returns the epa_echo_get_facility compliance profile at its default pages of inspections and drinking-water violations (inspectionsNextOffset and drinkingWaterViolationsNextOffset mark more; page them with the tool); errors when no facility resolves

epa://superfund/{site_id} resource

  • site_id comes from epa_superfund_search_sites
  • Returns the same data as epa_superfund_get_site, with site fields at the top level and an additive contaminants array; 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, ACS 2018–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, triFacilityIds, caseId, and pwsid
  • Every search pages with offset and nextOffset, and epa_echo_get_facility pages its inspections and drinking-water violations with inspections_offset and drinking_water_violations_offset; a limit above a tool's cap, up to 5,000, returns the cap with a limitNotice
  • Searches return effectiveQuery, the filters as the server sent them upstream, so a result states what it matched
  • epa_echo_get_facility returns a partial profile with a notice when the status lookup fails
  • Empty searches return a message that 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 10 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 10 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 10 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:

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

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). Eleven 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 case search and report, current drinking-water violators. | | 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/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.