@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.
Maintainers
Readme
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; needsstate), 5-digitcounty_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) andnaics(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 alimitNotice. Page withoffsetandnextOffset;totalCountis the match beforelimitandoffset - Returns
registryIdforepa_echo_get_facility,triFacilityIdsforepa_tri_get_facility_releases, county andfipsCode, coordinates, per-program flags, and five-year inspection and penalty totals.effectiveQueryechoes the filters as sent programstakes one of CAA, CWA, RCRA, TRI, or SDWA;has_violationkeeps facilities in significant noncompliance;active_onlykeeps facilities with an active permit
epa_echo_get_facility tool
registry_idis a Registry ID fromepa_echo_search_facilitiesor a program ID ECHO resolves to one: a PWSID fromepa_water_search_systems, a TRI facility ID, an NPDES permit, or a RCRA handler ID. The profile carries the resolvedregistryId- Returns
complianceStatus, thenaicscodes its programs record, a per-statuteprogramCompliancesummary, five years of enforcement actions with penalties (caseIdfeedsepa_echo_get_case), the latest TRI total withtriReleasesTransfersYear, andtriFacilityIdsforepa_tri_get_facility_releases - Two paged lists: inspections from the last five years, newest first (
inspections_limitdefault and cap 100,inspections_offset;inspectionsTotalcounts them all andinspectionsNextOffsetcontinues), anddrinkingWaterViolationsfor a public water system, newest compliance period first, each with contaminant, measured level, MCL, status, and enforcement actions (drinking_water_violations_limitdefault 50, cap 200,drinking_water_violations_offset;drinkingWaterViolationsTotalanddrinkingWaterViolationsNextOffset). A largerlimitreturns the cap with alimitNotice, andtruncatedis true while either list has more - If the status lookup fails, the profile returns without
complianceStatusand anoticesays so; an unknown ID fails asfacility_not_found
epa_echo_search_violations tool
- Requires
stateorzip_code; up to 100 cases per call (default 50), and a largerlimitup to 5,000 returns 100 with alimitNotice. Page withoffsetandnextOffset;effectiveQueryechoes the filters as sent programfilters CAA, CWA, RCRA, SDWA, CERCLA, EPCRA, 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- Returns
caseId,caseName,caseType,caseStatus,enforcementOutcome,leadAgency, the primary law (programsViolated) andprimarySection, 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
caseIdtoepa_echo_get_case
epa_echo_get_case tool
case_idis a civil case number: acaseIdfromepa_echo_search_violations(such as03-2014-7010) or a formal action'scaseIdfromepa_echo_get_facility. A criminalCR_number fails ascriminal_casebefore any request; an unknown number fails ascase_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, andmilestones facilitieslists the facilities the case names, one per Registry ID, with address, NAICS and SIC codes, and the program IDs the case links; pass aregistryIdtoepa_echo_get_facilityenforcementConclusionslists 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 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(a real YYYY-MM-DD date) 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_tri_get_facility_releases tool
facility_idis the TRIfacilityIdfromepa_tri_search_releasesorepa_echo_get_facilitytriFacilityIds; optionalyear(1987–2030) and partialchemical_name- Returns per-chemical on-site releases to air, water, land, and underground injection, their sum
totalOnSiteReleasesInLbs, and the Form R Section 8.8oneTimeReleaseInLbs, which the media quantities already include, newest reporting year first limitdefaults to and is capped at 500 filings; a larger value up to 5,000 returns 500 with alimitNotice. Page withoffsetandnextOffset;truncated,shown,cap, andguidancemark 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); optionalcounty(case-insensitive partial match on the bare name; a trailing County, Parish, Borough, City and Borough, Census Area, or Municipality is dropped), partialchemical_name, andyear; newest reporting year first whenyearis omitted - Up to 200 records per call (default 50); a larger
limitup to 5,000 returns 200 with alimitNotice. Page withoffsetandnextOffset;effectiveQueryechoes the filters as sent. Records come in reporting-year order, not ranked by quantity - Returns
facilityIdforepa_tri_get_facility_releases,registryIdforepa_echo_get_facility,parentCompany, and the Form R Section 8.8oneTimeReleaseInLbs;include_release_breakdown: trueadds on-site releases to air, water, land, and underground injection plustotalOnSiteReleasesInLbs, 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), orlatitude+longitude+radius_miles(0.1–500); up to 200 sites per call (default 50), and a largerlimitup to 5,000 returns 200 with alimitNotice. Page withoffsetandnextOffset;effectiveQueryechoes 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_statusfilters listed (F/A), not-listed (N), proposed (P), deleted (D), removed (R), withdrawn (W), or all (default) exclude_archived: truerequires SEMSarchived_ind=Nbefore 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; follownextOffset - Proximity searches rescan every latitude/longitude candidate on each call, apply exact distance, and page the nearest-first order (site ID breaks ties).
totalCountis the number of matching sites, and anoticegives 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_idfromepa_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) orzip_code; up to 200 systems per call (default 50) in PWSID order, and a largerlimitup to 5,000 returns 200 with alimitNotice. Page withoffsetandnextOffset;totalCountappears when the call saw the whole match;effectiveQueryechoes the filters as sent zip_codematches each system's address of record by prefix, so stored ZIP+4s match, and not its service area.county(needsstate) 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, soHarrisdoes not match Harrisonhas_violationkeeps 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_typeis community, non-transient, or transient, returned as CWS / NTNCWS / TNCWS intype;active_onlykeeps systems SDWIS lists as active- A ZIP or county
has_violationcheck reads up to 1,000 systems per call; when that fills before the page does, anoticesays so andnextOffsetcontinues - Pass a returned
pwsidasregistry_idtoepa_echo_get_facilityfor the system's drinking-water violations
epa_ejscreen_get_scores 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, plus the
demographicIndexandsupplementalDemographicIndex, each with itsusAverage. Both indexes are unitless, shifted means of z-scores, not percentages; an indicator'sejIndexis the Demographic Index × its US percentile, population-weighted over the buffer coverage.valid: falsewith anotewhen 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_idis a Registry ID fromepa_echo_search_facilitiesor a program ID ECHO resolves to one, such as a PWSID fromepa_water_search_systems- Returns the
epa_echo_get_facilitycompliance profile at its default pages of inspections and drinking-water violations (inspectionsNextOffsetanddrinkingWaterViolationsNextOffsetmark more; page them with the tool); errors when no facility resolves
epa://superfund/{site_id} resource
site_idcomes fromepa_superfund_search_sites- Returns the same data as
epa_superfund_get_site, with site fields at the top level and an additivecontaminantsarray; 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, andpwsid - Every search pages with
offsetandnextOffset, andepa_echo_get_facilitypages its inspections and drinking-water violations withinspections_offsetanddrinking_water_violations_offset; alimitabove a tool's cap, up to 5,000, returns the cap with alimitNotice - Searches return
effectiveQuery, the filters as the server sent them upstream, so a result states what it matched epa_echo_get_facilityreturns a partial profile with anoticewhen the status lookup fails- Empty searches return a
messagethat 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/mcpPrerequisites
- 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
- 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 toolConfiguration
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:httpRun 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-serverThe 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/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 testLicense
Apache-2.0 — see LICENSE for details.
