@cyanheads/epa-mcp-server 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/AGENTS.md +413 -0
  2. package/CLAUDE.md +91 -77
  3. package/Dockerfile +14 -7
  4. package/LICENSE +1 -1
  5. package/README.md +109 -97
  6. package/changelog/0.3.x/0.3.1.md +27 -0
  7. package/changelog/0.3.x/0.3.2.md +40 -0
  8. package/changelog/template.md +67 -43
  9. package/dist/config/server-config.d.ts +2 -2
  10. package/dist/config/server-config.d.ts.map +1 -1
  11. package/dist/config/server-config.js +6 -8
  12. package/dist/config/server-config.js.map +1 -1
  13. package/dist/index.js +2 -12
  14. package/dist/index.js.map +1 -1
  15. package/dist/mcp-server/tools/definitions/get-ejscreen.tool.d.ts +2 -1
  16. package/dist/mcp-server/tools/definitions/get-ejscreen.tool.d.ts.map +1 -1
  17. package/dist/mcp-server/tools/definitions/get-ejscreen.tool.js +1 -0
  18. package/dist/mcp-server/tools/definitions/get-ejscreen.tool.js.map +1 -1
  19. package/dist/mcp-server/tools/definitions/index.d.ts +12 -20
  20. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
  21. package/dist/mcp-server/tools/definitions/search-facilities.tool.d.ts +1 -6
  22. package/dist/mcp-server/tools/definitions/search-facilities.tool.d.ts.map +1 -1
  23. package/dist/mcp-server/tools/definitions/search-facilities.tool.js +0 -6
  24. package/dist/mcp-server/tools/definitions/search-facilities.tool.js.map +1 -1
  25. package/dist/mcp-server/tools/definitions/search-violations.tool.d.ts +3 -3
  26. package/dist/services/airnow/airnow-service.d.ts.map +1 -1
  27. package/dist/services/airnow/airnow-service.js +2 -2
  28. package/dist/services/airnow/airnow-service.js.map +1 -1
  29. package/dist/services/dmap/dmap-service.d.ts.map +1 -1
  30. package/dist/services/dmap/dmap-service.js +57 -41
  31. package/dist/services/dmap/dmap-service.js.map +1 -1
  32. package/dist/services/echo/echo-service.d.ts.map +1 -1
  33. package/dist/services/echo/echo-service.js +32 -10
  34. package/dist/services/echo/echo-service.js.map +1 -1
  35. package/dist/services/echo/types.d.ts +1 -0
  36. package/dist/services/echo/types.d.ts.map +1 -1
  37. package/dist/services/ejscreen/ejscreen-service.d.ts.map +1 -1
  38. package/dist/services/ejscreen/ejscreen-service.js +6 -8
  39. package/dist/services/ejscreen/ejscreen-service.js.map +1 -1
  40. package/package.json +16 -14
  41. package/server.json +4 -4
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  <div align="center">
2
2
  <h1>@cyanheads/epa-mcp-server</h1>
3
- <p><b>Access EPA environmental data — facility compliance (ECHO), toxic releases (TRI), Superfund sites, drinking water systems, environmental justice screening (EJScreen), and real-time air quality (AirNow) via MCP. STDIO or Streamable HTTP.</b>
4
- <div>9 Tools • 2 Resources</div>
3
+ <p><b>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.</b>
5
4
  </p>
5
+ <div>9 Tools • 2 Resources</div>
6
6
  </div>
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-0.3.0-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/epa-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.29.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/epa-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/epa-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^6.0.3-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-0.3.2-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/epa-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/epa-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/epa-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -21,151 +21,143 @@
21
21
 
22
22
  ---
23
23
 
24
- ## Tools
24
+ ## Overview
25
25
 
26
- 9 tools spanning EPA facility compliance, toxic chemical releases, Superfund cleanup sites, drinking water safety, environmental justice screening, and real-time air quality:
26
+ EPA environmental data across five federal programs — facility compliance (ECHO), toxic chemical releases (TRI), Superfund cleanup sites, drinking water systems (SDWIS), and environmental-justice screening (EJScreen) — plus real-time air quality via AirNow. Search facilities by location or compliance status, pull inspection and enforcement history, track toxic releases across a region, and screen a point for environmental-justice risk from any MCP client. Runs as a stdio process or a local Streamable HTTP server.
27
+
28
+ ### Tools
27
29
 
28
30
  | Tool | Description |
29
31
  |:---|:---|
30
- | `epa_search_facilities` | Search EPA-regulated facilities by location (ZIP, state, city, or latitude/longitude + radius proximity), industry, or compliance status across all environmental programs (CAA, CWA, RCRA, TRI, SDWA). Returns facility name, EPA Registry ID, coordinates, county FIPS, per-program compliance flags, inspection counts, penalty totals, and TRI release totals. |
31
- | `epa_get_facility` | Retrieve a full compliance profile for a single EPA-regulated facility: compliance status per program, inspection dates, formal enforcement actions, penalty amounts, and TRI annual release totals. Aggregates multiple ECHO DFR endpoints in parallel. |
32
- | `epa_search_violations` | Search EPA civil and criminal enforcement cases by state, regulatory program, or date range. Returns case identifier, facility name and Registry ID, programs involved, penalty assessed, settlement date, and case type. |
33
- | `epa_get_air_quality` | Get AQI observations or forecasts for a location. Returns per-pollutant AQI values (PM2.5, ozone, CO, SO2, NO2), AQI category (Good through Hazardous), reporting area name, and observation timestamp. |
34
- | `epa_get_tri_releases` | Query Toxic Release Inventory annual chemical release data for a specific facility by medium (air, water, land, underground injection) and reporting year. |
35
- | `epa_search_tri_releases` | Search Toxic Release Inventory data across facilities in a state or county for a given year. Returns facility name, TRI ID, chemical name, total releases by medium, and facility coordinates. |
36
- | `epa_search_superfund` | Search Superfund (CERCLA/SEMS) sites by location or NPL listing status. Accepts state/city/ZIP or lat/lng + radius for proximity searches. Returns site name, EPA ID, NPL status, cleanup status, and coordinates. |
37
- | `epa_search_water_systems` | Search drinking water systems (SDWIS) by state or ZIP code. Returns system name, PWSID, population served, primary water source, and active violation status. |
38
- | `epa_get_ejscreen` | Get EJScreen environmental-justice indicators for a point + buffer: 13 environmental and 6 demographic indicators with national/state percentiles, EJ Index values, and the demographic indices. Serves EJScreen v2.2 (2022) via the community-maintained EJAM API (Public Environmental Data Partners), which rehosts EJScreen after EPA discontinued public access in 2025. |
39
-
40
- ### `epa_search_facilities`
41
-
42
- Search for EPA-regulated facilities with cross-program compliance data.
43
-
44
- - Geographic filters: ZIP code, state, city (city requires state), or latitude + longitude + radius_miles for proximity search
45
- - Program filter: narrow to CAA, CWA, RCRA, TRI, or SDWA registrants
46
- - Compliance filter: `has_violation` flag to surface only non-compliant facilities
47
- - Returns `RegistryID` (key for `epa_get_facility`), `FacFIPSCode` (county FIPS for Census chaining), and coordinates
48
- - Results cap enforced — unscoped searches are prohibited at the input validation layer
32
+ | `epa_search_facilities` | Search EPA-regulated facilities by location, industry program, or compliance status across CAA, CWA, RCRA, TRI, and SDWA |
33
+ | `epa_get_facility` | Full compliance profile for one facility by EPA Registry ID — inspections, enforcement actions, and penalties |
34
+ | `epa_search_violations` | Search EPA civil and criminal enforcement cases by state, program, or date range |
35
+ | `epa_get_air_quality` | Current AQI observations or next-day forecasts from AirNow |
36
+ | `epa_get_tri_releases` | Per-chemical Toxic Release Inventory data for a single facility |
37
+ | `epa_search_tri_releases` | Toxic Release Inventory records across facilities in a state or county |
38
+ | `epa_search_superfund` | Search Superfund (CERCLA/SEMS) sites by location or NPL listing status |
39
+ | `epa_search_water_systems` | Search drinking water systems (SDWIS) by state or ZIP code |
40
+ | `epa_get_ejscreen` | EJScreen environmental-justice indicators for a point and buffer |
41
+
42
+ ### Resources
43
+
44
+ | Resource | Description |
45
+ |:---|:---|
46
+ | `epa://facility/{registry_id}` | Full compliance profile for a facility by EPA Registry ID (same data as `epa_get_facility`) |
47
+ | `epa://superfund/{site_id}` | Superfund site record by SEMS site ID |
49
48
 
50
- ---
49
+ All resource data is also reachable via tools — use `epa_get_facility` and `epa_search_superfund` for programmatic access in tool-only MCP clients.
51
50
 
52
- ### `epa_get_facility`
51
+ ## Capability reference
53
52
 
54
- Retrieve a comprehensive compliance profile by EPA Registry ID.
53
+ ### `epa_search_facilities` <sub>tool</sub>
55
54
 
56
- - Aggregates 3–5 ECHO DFR endpoints in parallel: program flags and TRI totals, compliance summary, inspection/enforcement history, CAA details (if registered), CWA/NPDES permit details (if registered)
57
- - Uses `Promise.allSettled` — partial data returned even if one upstream endpoint fails
58
- - Includes formal enforcement actions, penalty amounts, and inspection dates across all programs
55
+ - Geographic filters: ZIP code, state, city (pair with state), or latitude + longitude + radius_miles (max 100 miles) for proximity search — at least one is required
56
+ - Optional `programs` filter narrows to CAA, CWA, RCRA, TRI, or SDWA registrants; `has_violation` surfaces only non-compliant facilities
57
+ - Returns `registryId` for `epa_get_facility`, plus `fipsCode` when available for Census chaining
58
+ - Up to 100 results per call (default 50)
59
59
 
60
60
  ---
61
61
 
62
- ### `epa_search_violations`
63
-
64
- Search area-level EPA enforcement cases — distinct from per-facility history in `epa_get_facility`.
62
+ ### `epa_get_facility` <sub>tool</sub>
65
63
 
66
- - Program filter: CAA, CWA, RCRA, SDWA, CERCLA, FIFRA, or TSCA
67
- - Case type: civil, criminal, or all
68
- - Date range filtering by filing date (ISO 8601)
69
- - Returns case identifier, affected facility name and Registry ID for downstream `epa_get_facility` lookup
64
+ - Input: `registry_id`, obtained from `epa_search_facilities`
65
+ - Aggregates 3–5 ECHO DFR endpoints in parallel — program flags and TRI totals, compliance summary, inspection/enforcement history, CAA details, CWA/NPDES permit details
66
+ - Uses `Promise.allSettled` — partial data is returned even when one upstream endpoint fails
67
+ - `airCompliance` / `waterCompliance` are present only when the facility is registered under that program
68
+ - `facility_not_found` when ECHO has no record for the Registry ID
70
69
 
71
70
  ---
72
71
 
73
- ### `epa_get_air_quality`
72
+ ### `epa_search_violations` <sub>tool</sub>
74
73
 
75
- Get current AQI observations or daily forecasts from AirNow.
76
-
77
- - Accepts ZIP code or latitude/longitude coordinates
78
- - `mode: current` returns the latest observed AQI per pollutant; `mode: forecast` returns daily AQI forecasts (requires `forecast_date`)
79
- - AQI categories: Good (1) through Hazardous (6) with numeric and text category
80
- - Data is preliminary — suitable for awareness, not regulatory or enforcement decisions
81
- - AirNow responses cached at ~1 hour TTL to respect rate limits
74
+ - At least one of `state` or `zip_code` is required
75
+ - `program` filter covers CAA, CWA, RCRA, SDWA, CERCLA, FIFRA, or TSCA; `case_type` is civil, criminal, or all (default all)
76
+ - Date range filter by filing date (ISO 8601 `date_filed_start` / `date_filed_end`)
77
+ - `facilityName` and `registryId` are not populated by ECHO's enforcement-case endpoint — chain into `epa_get_facility` for facility detail
78
+ - Up to 100 cases per call (default 50)
82
79
 
83
80
  ---
84
81
 
85
- ### `epa_get_tri_releases`
86
-
87
- Query per-chemical release breakdown for a single TRI facility.
82
+ ### `epa_get_air_quality` <sub>tool</sub>
88
83
 
89
- - Accepts TRI facility ID from `epa_search_facilities` results
90
- - Returns release quantities by medium: air, water, land, underground injection
91
- - Optional year filter (defaults to most recent available); optional chemical name filter
92
- - TRI data lags ~18 months — most recent available year is typically 2 years prior to current
84
+ - Provide `zip_code` or both `latitude` and `longitude`
85
+ - `mode`: `current` (default) or `forecast` (requires `forecast_date`, ISO 8601)
86
+ - Per-pollutant AQI (PM2.5, ozone, CO, SO2, NO2) with numeric `categoryNumber` (1 Good – 6 Hazardous) and `categoryName`
87
+ - `distance_miles` sets the reporting-station search radius (default 25, max 300)
88
+ - Data is preliminary — informational use only, not for regulatory decisions; responses are cached ~1 hour
89
+ - Only registered when `AIRNOW_API_KEY` is set
93
90
 
94
91
  ---
95
92
 
96
- ### `epa_search_tri_releases`
93
+ ### `epa_get_tri_releases` <sub>tool</sub>
97
94
 
98
- Identify top polluters in a region via TRI data.
95
+ - `facility_id` is the TRI `facilityId` from `epa_search_tri_releases`; optional `year` (1987–2030, defaults to all available years) and `chemical_name` (partial match)
96
+ - Per-chemical breakdown by medium — air, water, land, underground injection — plus a separate one-time/non-routine release total
97
+ - TRI data lags ~18 months; the most recent available year is typically 2 years prior to the current year
99
98
 
100
- - State and optional county scope; required year parameter
101
- - Optional chemical name filter to focus on a specific substance
102
- - Returns facility coordinates for downstream map or proximity analysis
99
+ ---
100
+
101
+ ### `epa_search_tri_releases` <sub>tool</sub>
102
+
103
+ - `state` is required (2-letter); optional `county` (partial match), `year`, and `chemical_name`
104
+ - Up to 200 records per call (default 50); an enrichment flag marks the result truncated when it hits the limit
103
105
  - Complement to `epa_get_tri_releases` — use this for area discovery, then drill into a specific facility
104
106
 
105
107
  ---
106
108
 
107
- ### `epa_search_superfund`
108
-
109
- Search Superfund (CERCLA/SEMS) sites by location or proximity.
109
+ ### `epa_search_superfund` <sub>tool</sub>
110
110
 
111
- - Two input shapes: state/city/ZIP for administrative filters, or lat/lng + radius for proximity
112
- - NPL status filter: listed, not-listed, proposed, or all
113
- - Returns site cleanup status and coordinates for downstream spatial analysis
111
+ - Two input shapes: `state`/`city`/`zip_code`, or `latitude`+`longitude`+`radius_miles` (0.1–500 miles) — one is required
112
+ - `npl_status` filter: `listed`, `not-listed`, `proposed`, or `all` (default `all`)
113
+ - Up to 200 sites per call (default 50)
114
114
 
115
115
  ---
116
116
 
117
- ### `epa_search_water_systems`
117
+ ### `epa_search_water_systems` <sub>tool</sub>
118
118
 
119
- Identify drinking water systems with active or recent violations.
120
-
121
- - State and optional ZIP code scope
122
- - `has_violation` flag surfaces only systems with current violations
123
- - PWS type filter: community (`CWS`), non-transient non-community (`NTNCWS`), or transient non-community (`TNCWS`)
119
+ - At least one of `state` or `zip_code` is required
120
+ - `has_violation` surfaces only systems with active violations; `pws_type` filters to `community`, `non-transient`, or `transient` (output `type` reports the SDWIS codes CWS / NTNCWS / TNCWS)
121
+ - Up to 200 systems per call (default 50)
124
122
 
125
123
  ---
126
124
 
127
- ### `epa_get_ejscreen`
125
+ ### `epa_get_ejscreen` <sub>tool</sub>
128
126
 
129
- Screen a point and its surrounding buffer for environmental-justice indicators.
127
+ - Input: `latitude`, `longitude`, `distance` (default 1), and `unit` (`miles` or `kilometers`, default `miles`); kilometers are converted to miles before the request, and the buffer is capped at 15 miles
128
+ - Returns 13 environmental and 6 demographic indicators, each with national/state percentiles, plus the Demographic Index and Supplemental Demographic Index
129
+ - Points outside US coverage return `coverage.valid: false` with a note instead of fabricated indicators
130
+ - Data source: EJScreen v2.2 (2022) via the community-maintained EJAM API (Public Environmental Data Partners) — not a live EPA endpoint
130
131
 
131
- - Input: `latitude`, `longitude`, `distance` (default 1), and `unit` (`miles` or `kilometers`); kilometers are converted to miles before the request, and the buffer is capped at 15 miles
132
- - Returns the 13 EJScreen environmental indicators (PM2.5, ozone, diesel particulate, NO2, lead paint, traffic proximity, Superfund/RMP/hazardous-waste/wastewater proximity, underground storage tanks, drinking-water non-compliance, RSEI toxic air releases) and 6 demographic indicators (people of color, low income, limited English, less than high school, under 5, over 64)
133
- - Each indicator carries national and state percentiles plus EJ Index values; the Demographic Index and Supplemental Demographic Index are included
134
- - Points outside US coverage return a coverage note instead of fabricated indicators
135
- - **Data source:** EJScreen v2.2 (2022) served via the community-maintained [EJAM API](https://api.ejanalysis.com) (Public Environmental Data Partners), which rehosts EJScreen after EPA discontinued public access in 2025 — not a live EPA endpoint
132
+ ---
136
133
 
137
- ## Resources and prompts
134
+ ### `epa://facility/{registry_id}` <sub>resource</sub>
138
135
 
139
- | Type | Name | Description |
140
- |:---|:---|:---|
141
- | Resource | `epa://facility/{registry_id}` | Full compliance profile for a facility by EPA Registry ID (same data as `epa_get_facility`) |
142
- | Resource | `epa://superfund/{site_id}` | Superfund site record by SEMS site ID |
136
+ - Same data as `epa_get_facility`; `registry_id` comes from `epa_search_facilities`
137
+ - Errors when the Registry ID has no ECHO record
143
138
 
144
- All resource data is also reachable via tools. Use `epa_get_facility` and `epa_search_superfund` for programmatic access in tool-only MCP clients.
139
+ ---
145
140
 
146
- ## Features
141
+ ### `epa://superfund/{site_id}` <sub>resource</sub>
147
142
 
148
- Built on [`@cyanheads/mcp-ts-core`](https://www.npmjs.com/package/@cyanheads/mcp-ts-core):
143
+ - Same data as `epa_search_superfund` records; `site_id` comes from `epa_search_superfund`
144
+ - Errors when the site ID has no SEMS record
145
+
146
+ ## Features
149
147
 
150
- - Declarative tool and resource definitions — single file per primitive, framework handles registration and validation
151
- - Unified error handling — handlers throw, framework catches, classifies, and formats
152
- - Pluggable auth: `none`, `jwt`, `oauth`
153
- - Swappable storage backends: `in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`
154
- - Structured logging with optional OpenTelemetry tracing
155
- - STDIO and Streamable HTTP transports
148
+ Built on [`@cyanheads/mcp-ts-core`](https://github.com/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.
156
149
 
157
150
  EPA-specific:
158
151
 
159
- - Multiple environmental data sources unified behind a single `epa_` tool surface: ECHO (facility compliance), Envirofacts DMAP (TRI, Superfund, SDWIS), AirNow (real-time air quality), and the community-maintained EJAM API rehosting EJScreen environmental-justice data (v2.2, 2022)
152
+ - Multiple environmental data sources unified behind a single `epa_` tool surface: ECHO (facility compliance), Envirofacts DMAP (TRI, Superfund, SDWIS), AirNow (real-time air quality), and the community-maintained EJAM API rehosting EJScreen data (v2.2, 2022)
160
153
  - Parallel ECHO DFR aggregation in `epa_get_facility` — 3–5 upstream calls resolved concurrently with `Promise.allSettled`
161
154
  - AirNow response caching (~1 hour TTL) to stay within per-key rate limits
162
- - DMAP coordinate normalization — `tri.tri_facility` DDMMSS integers converted to decimal degrees
163
155
 
164
156
  Agent-friendly output:
165
157
 
166
- - Cross-tool join keys surfaced on every response — `RegistryID` and `FacFIPSCode` from facility search feed directly into compliance, TRI, and Census API workflows
167
- - Typed enforcement and compliance status fields — agents branch on data values, not string parsing
168
- - Structured partial failure — `epa_get_facility` returns available program data even when one DFR endpoint is unavailable, with per-section status
158
+ - Facility search supplies `registryId` for compliance lookups and `fipsCode` when available for Census queries; TRI search supplies `facilityId` for release details
159
+ - Structured partial failure — `epa_get_facility` returns available program data even when one DFR endpoint is unavailable, with `airCompliance`/`waterCompliance` present only when that program applies
160
+ - Recovery-hint messages on empty results — every search tool returns a `message` field that echoes the applied filters and suggests how to broaden the search
169
161
 
170
162
  ## Getting started
171
163
 
@@ -207,6 +199,25 @@ Or with npx (no Bun required):
207
199
  }
208
200
  ```
209
201
 
202
+ Or with Docker:
203
+
204
+ ```json
205
+ {
206
+ "mcpServers": {
207
+ "epa-mcp-server": {
208
+ "type": "stdio",
209
+ "command": "docker",
210
+ "args": [
211
+ "run", "-i", "--rm",
212
+ "-e", "MCP_TRANSPORT_TYPE=stdio",
213
+ "-e", "AIRNOW_API_KEY=your-airnow-key",
214
+ "ghcr.io/cyanheads/epa-mcp-server:latest"
215
+ ]
216
+ }
217
+ }
218
+ }
219
+ ```
220
+
210
221
  For Streamable HTTP, set the transport and start the server:
211
222
 
212
223
  ```sh
@@ -216,7 +227,7 @@ MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 AIRNOW_API_KEY=... bun run start:http
216
227
 
217
228
  ### Prerequisites
218
229
 
219
- - [Bun v1.3.0](https://bun.sh/) or higher (or Node.js v24+).
230
+ - [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
220
231
  - (Optional) An AirNow API key to enable `epa_get_air_quality` — register free at [docs.airnowapi.org/account/request](https://docs.airnowapi.org/account/request/). Without it the server runs the other 8 tools. ECHO, DMAP, and EJScreen tools require no API key.
221
232
 
222
233
  ### Installation
@@ -260,6 +271,7 @@ All configuration is validated at startup via Zod schemas in `src/config/`. Key
260
271
  | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http` | `stdio` |
261
272
  | `MCP_HTTP_PORT` | HTTP server port | `3010` |
262
273
  | `MCP_HTTP_ENDPOINT_PATH` | HTTP endpoint path | `/mcp` |
274
+ | `MCP_SESSION_MODE` | HTTP sessions: `auto`, `stateful`, or `stateless`. Explicit environment values override the server default; `auto` resolves to `stateful`. Tenant-scoped caching is independent of sessions. | `stateless` |
263
275
  | `MCP_AUTH_MODE` | Auth mode: `none`, `jwt`, or `oauth` | `none` |
264
276
  | `MCP_LOG_LEVEL` | Log level (RFC 5424) | `info` |
265
277
  | `LOGS_DIR` | Directory for log files (Node.js only) | `<project-root>/logs` |
@@ -327,7 +339,7 @@ See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rule
327
339
 
328
340
  ## Contributing
329
341
 
330
- Issues and pull requests are welcome. Run checks and tests before submitting:
342
+ Issues are welcome. Run checks and tests before submitting:
331
343
 
332
344
  ```sh
333
345
  bun run devcheck
@@ -0,0 +1,27 @@
1
+ ---
2
+ summary: "Modern MCP protocol support and bounded EPA search pagination."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.3.1 — 2026-08-22
8
+
9
+ ## Changed
10
+
11
+ - **MCP protocol** — adopts `@cyanheads/mcp-ts-core` ^0.12.3 and MCP SDK v2: modern MCP protocol support, undeclared tool input fields are rejected, and error results advertise a validation-compatible envelope.
12
+ - **Packaging** — updates the Bun build images and package manager pin to 1.4.0, and removes platform-specific native bindings from MCPB bundles.
13
+
14
+ ## Fixed
15
+
16
+ - **`epa_search_tri_releases`** batches facility IDs to avoid oversized URLs and respects `limit`.
17
+ - **`epa_search_violations`, `epa_search_facilities`, and `epa_search_water_systems`** honor `limit` while preserving full-match totals where the upstream exposes them.
18
+
19
+ ## Dependencies
20
+
21
+ - `@cyanheads/mcp-ts-core` ^0.10.14 → ^0.12.3
22
+ - `@biomejs/biome` ^2.5.2 → ^2.5.9
23
+ - `@types/node` ^25.9.3 → ^26.2.0
24
+ - `ignore` ^7.0.5 → ^7.0.6
25
+ - `tsc-alias` ^1.8.17 → ^1.9.2
26
+ - `typescript` ^6.0.3 → ^7.0.2
27
+ - `vitest` ^4.1.9 → ^4.1.11
@@ -0,0 +1,40 @@
1
+ ---
2
+ summary: "Stateless HTTP defaults, private upstream errors, and mcp-ts-core 0.13.6 adoption."
3
+ breaking: true
4
+ security: true
5
+ agent-notes: |
6
+ Bun 1.4 or later is required. Development workflows now live under
7
+ framework-skills/ rather than skills/.
8
+ ---
9
+
10
+ # 0.3.2 — 2026-09-19
11
+
12
+ ## Changed
13
+
14
+ - HTTP defaults to stateless when MCP_SESSION_MODE is unset; an explicit environment value still overrides it (#22).
15
+ - **Environment configuration** treats empty and whole-placeholder values as unset through `@cyanheads/mcp-ts-core`, so optional AirNow configuration stays disabled and the EJScreen endpoint uses its default.
16
+ - **Runtime baseline** now requires Bun 1.4 or later; this is a runtime migration with unchanged tool contracts.
17
+ - **Tool arguments** accept case-style aliases for declared keys and ignore the framework's fixed client metadata-key list; other unknown keys still return `InvalidParams`.
18
+ - **Error responses** include reason and retryability terms when available and preserve recovery guidance for client-side branching.
19
+ - **Development workflows** moved from `skills/` to `framework-skills/`, and package, bundle, and manifest checks follow the current framework delivery rules.
20
+ - **Project documentation and issue forms** now describe the resource surface, reporting route, security advisory path, and current runtime values.
21
+
22
+ ## Fixed
23
+
24
+ - **AirNow, ECHO, DMAP, and EJAM failures** no longer expose upstream request URLs in MCP error data. [#23](https://github.com/cyanheads/epa-mcp-server/issues/23)
25
+ - **Invalid tool arguments** now use the framework's production `InvalidParams` error envelope with structured error content.
26
+ - **EJAM non-400 HTTP failures** now use the framework's upstream HTTP classification, while invalid coordinates retain their validation error.
27
+
28
+ ## Security
29
+
30
+ - **AirNow error handling** no longer returns a credential-bearing upstream URL to clients. [#23](https://github.com/cyanheads/epa-mcp-server/issues/23)
31
+
32
+ ## Dependencies
33
+
34
+ - `@cyanheads/mcp-ts-core` `^0.12.3` → `^0.13.6`
35
+ - `zod` `^4.4.3` → `^4.6.5`
36
+ - `@biomejs/biome` `^2.5.9` → `^2.5.14`
37
+ - `@types/node` `^26.2.0` → `^26.6.1`
38
+ - `ignore` `^7.0.6` → `^7.0.9`
39
+ - `tsc-alias` `^1.9.2` → `^1.9.5`
40
+ - `vitest` `^4.1.11` → `^5.0.1`
@@ -4,10 +4,11 @@
4
4
  # to author a new release. Set that file's H1 to `# <version> — YYYY-MM-DD`
5
5
  # with a concrete date.
6
6
 
7
- # Required. One-line GitHub Release-style headline. 350 character cap.
8
- # Default short and scannable. Don't pad, don't stitch unrelated changes with
9
- # semicolons — pick the headline. Quotes required: unquoted YAML treats `: `
10
- # inside the value as a key separator and fails GitHub's strict parser.
7
+ # Required. One-line GitHub Release-style headline. 350 character cap — a
8
+ # ceiling, not a target. Default short and scannable. Don't pad, don't stitch
9
+ # unrelated changes with commas/semicolons into an inventory — pick the one
10
+ # headline the release is about. Quotes required: unquoted YAML treats
11
+ # `: ` inside the value as a key separator and fails GitHub's strict parser.
11
12
  summary: ""
12
13
 
13
14
  # Set `true` when consumers must change code to upgrade: API removals,
@@ -15,16 +16,19 @@ summary: ""
15
16
  # usage. Flagged as `Breaking` in the rollup.
16
17
  breaking: false
17
18
 
18
- # Set `true` if this release contains any security fix. Pairs with the
19
- # `## Security` section below. Flagged as `Security` in the rollup so
20
- # users can triage upgrade urgency at a glance.
19
+ # Set `true` ONLY for a security fix in THIS project's own source code — a
20
+ # vulnerability or hardening in code you ship. A dependency or transitive CVE
21
+ # bump is routine maintenance, NOT a security release: record it under
22
+ # `## Dependencies` (with the advisory ID) and leave this `false`. When true,
23
+ # pairs with the `## Security` section below and flags `Security` in the rollup.
21
24
  security: false
22
25
 
23
26
  # Optional free-form notes for maintenance agents processing this release.
24
27
  # Not rendered in CHANGELOG — consumed by agents running `maintenance` on
25
- # downstream servers. Use for adoption instructions that don't fit the
26
- # human-facing sections: new files to create, fields to populate, one-time
27
- # migration steps. Omit the field entirely when there's nothing to say.
28
+ # downstream servers. ADOPTION STEPS ONLY — new files to create, fields to
29
+ # populate, one-time migration steps. Never a second rendering of the body:
30
+ # if a body bullet already says it, name the bullet's symbol instead of
31
+ # re-explaining. Omit the field entirely when there's nothing to say.
28
32
  # agent-notes: |
29
33
  # <instructions for downstream maintenance agents>
30
34
  ---
@@ -39,17 +43,54 @@ security: false
39
43
  each bullet with the symbol or concept name in **bold** so they can skip
40
44
  what's irrelevant and zoom in on what's not.
41
45
 
42
- Tone: terse, fact-dense, not verbose. Default to one sentence per bullet —
43
- name the symbol, state what changed, stop. Use a second sentence only when
44
- it carries weight. If a bullet feels long, it is.
45
-
46
- Cut: mechanism walkthroughs (those belong in JSDoc, CLAUDE.md/AGENTS.md, or the
47
- relevant skill), ceremonial framings ("This release introduces…",
48
- backwards-compat paragraphs), file-by-file test enumerations, internal
49
- implementation notes. Prefer code/symbol names over English re-explanations.
46
+ Tone: terse, fact-dense, not verbose. Bullet shape: **symbol** + what
47
+ changed + at most one consumer-facing caveat. One sentence by default, two
48
+ when the second carries weight — a bullet past ~40 words or three sentences
49
+ is wrong. The depth lives one hop away: the linked issue carries the why,
50
+ the commit diff carries the how. The changelog names what changed and what
51
+ a consumer does about it; a reader who wants mechanism opens the link.
52
+
53
+ Model length on THIS guide, never on the previous entry — entries modeled
54
+ on entries compound.
55
+
56
+ Cut (each has shipped as a wall of text; these are the cruft):
57
+ - History/justification narration — how the bug worked, why the old
58
+ behavior was wrong. One short clause at most; the issue carries the story.
59
+ - Design-rationale defense — "chosen over Y because…", "guarding the
60
+ getter is not enough…". That is the author arguing with a reviewer;
61
+ reviewers read the PR, not the changelog.
62
+ - Defensive unchanged-clauses — "X is unchanged", "byte-identical to
63
+ <prev>". Keep one only where its absence would cause a real misread,
64
+ as a short parenthetical.
65
+ - Edge-case inventories — marker lists, not-flagged lists, escape tables.
66
+ Tests and the issue carry those.
67
+ - Mechanism walkthroughs (JSDoc, CLAUDE.md/AGENTS.md, or the relevant
68
+ skill own those), ceremonial framings ("This release introduces…"),
69
+ backwards-compat paragraphs, file-by-file test enumerations. Prefer
70
+ code/symbol names over English re-explanations.
71
+
72
+ Verified ≠ included: the every-claim-verified-from-the-diff rule bounds
73
+ the TRUTH of what you write, never the AMOUNT.
74
+
75
+ Example — same fact, right size:
76
+
77
+ TOO LONG: **`fetchWithTimeout`'s `timeoutMs` bounds the whole exchange**
78
+ (#341). `fetch` resolves once headers arrive and the deadline was
79
+ cleared as the helper returned, so a peer that answered promptly and
80
+ then stalled the stream held the request open indefinitely. A 2xx
81
+ carrying a body now comes back as a passthrough wrapper that disarms
82
+ the deadline when the body closes, errors, or is cancelled; …
83
+ [+90 more words of mechanism and edge cases]
84
+
85
+ RIGHT: **`fetchWithTimeout`'s `timeoutMs` now bounds the whole
86
+ exchange, not just the headers** (#341). A stalled body aborts with
87
+ the same `Timeout` error; the returned `Response` is a wrapper, so
88
+ identity assertions (`toBe(response)`) no longer hold.
50
89
 
51
90
  Narrative intro: skip by default. Add one short sentence only when the
52
- release theme genuinely needs framing the bullets can't carry.
91
+ release theme genuinely needs framing the bullets can't carry. When many
92
+ bullets share one upgrade consequence, state it ONCE — intro line or
93
+ agent-notes — never per bullet.
53
94
 
54
95
  Sections: Keep a Changelog order — Added, Changed, Deprecated, Removed,
55
96
  Fixed, Security. Include only sections with entries; delete the rest
@@ -76,30 +117,13 @@ security: false
76
117
  in that unrelated item's metadata.
77
118
 
78
119
  TAG ANNOTATIONS — the annotated tag body renders as the GitHub Release body
79
- via `gh release create --notes-from-tag`. The tag is a derivative of this
80
- changelog entry — a condensed, scannable version, not a copy. Format:
81
-
82
- <theme — omit version number, GitHub prepends it>
83
- ← blank line
84
- <1-2 sentence context: what this release does>
85
- ← blank line
86
- Dependency bumps: ← section header
87
- ← blank line
88
- - `@cyanheads/mcp-ts-core` ^0.9.1 → ^0.9.6 ← bullet
89
- ← blank line
90
- Changed: ← only sections with entries
91
- ← blank line
92
- - `format()` output includes `query` in text mode
93
- ← blank line
94
- Added:
95
- ← blank line
96
- - `manifest.json` scaffolded for MCPB bundle support
97
- - Install badges (Claude Desktop, Cursor, VS Code)
98
- ← blank line
99
- <N> tests pass; `bun run devcheck` clean. ← footer
100
-
101
- Never a flat comma-separated string. Always structured markdown with
102
- sections. The tag must scan well as a rendered GitHub Release page.
120
+ via `gh release create --notes-from-tag`. It is a condensed digest of this
121
+ entry, never a copy, and its format is owned by the `release-and-publish`
122
+ skill (step 4, "Create the annotated tag"): a short subject line without the
123
+ version, flat headline bullets — no Keep-a-Changelog section headers, no
124
+ gates line — at most one deps line, issue backlinks, and the changelog link
125
+ last. In release-PR mode the `git-wrapup` skill authors those bullets as the
126
+ PR body's `## Changes` and the tag copies them.
103
127
  -->
104
128
 
105
129
  ## Added
@@ -4,11 +4,11 @@
4
4
  */
5
5
  import { z } from '@cyanheads/mcp-ts-core';
6
6
  declare const ServerConfigSchema: z.ZodObject<{
7
- airNowApiKey: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
7
+ airNowApiKey: z.ZodOptional<z.ZodString>;
8
8
  echoBaseUrl: z.ZodDefault<z.ZodString>;
9
9
  dmapBaseUrl: z.ZodDefault<z.ZodString>;
10
10
  airNowBaseUrl: z.ZodDefault<z.ZodString>;
11
- ejscreenBaseUrl: z.ZodPreprocess<z.ZodDefault<z.ZodString>>;
11
+ ejscreenBaseUrl: z.ZodDefault<z.ZodString>;
12
12
  }, z.core.$strip>;
13
13
  export type ServerConfig = z.infer<typeof ServerConfigSchema>;
14
14
  export declare function getServerConfig(): ServerConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB;;;;;;iBAoCtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CAS9C"}
1
+ {"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB;;;;;;iBA4BtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CAS9C"}
@@ -5,11 +5,10 @@
5
5
  import { z } from '@cyanheads/mcp-ts-core';
6
6
  import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
7
7
  const ServerConfigSchema = z.object({
8
- // Optional: an empty or whitespace-only value (e.g. an unfilled `${AIRNOW_API_KEY}`
9
- // placeholder from a bundle/compose config) is treated as absent so the server still
10
- // starts. When absent, index.ts skips registering epa_get_air_quality and AirNow init.
11
8
  airNowApiKey: z
12
- .preprocess((v) => (typeof v === 'string' && v.trim() === '' ? undefined : v), z.string().min(1).optional())
9
+ .string()
10
+ .min(1)
11
+ .optional()
13
12
  .describe('AirNow API key (free registration at docs.airnowapi.org). Optional — when unset, epa_get_air_quality is not registered and the other tools run without it.'),
14
13
  echoBaseUrl: z
15
14
  .string()
@@ -26,11 +25,10 @@ const ServerConfigSchema = z.object({
26
25
  .url()
27
26
  .default('https://www.airnowapi.org/aq')
28
27
  .describe('AirNow API base URL'),
29
- // Optional override. Bundle/compose configs may inject an unfilled
30
- // `${EJSCREEN_API_BASE_URL}` placeholder as an empty string — treat empty or
31
- // whitespace-only as absent so the default applies instead of failing `.url()`.
32
28
  ejscreenBaseUrl: z
33
- .preprocess((v) => (typeof v === 'string' && v.trim() === '' ? undefined : v), z.string().url().default('https://api.ejanalysis.com'))
29
+ .string()
30
+ .url()
31
+ .default('https://api.ejanalysis.com')
34
32
  .describe('EJScreen (EJAM) API base URL — the community-maintained EJScreen rehost'),
35
33
  });
36
34
  let _config;
@@ -1 +1 @@
1
- {"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,oFAAoF;IACpF,qFAAqF;IACrF,uFAAuF;IACvF,YAAY,EAAE,CAAC;SACZ,UAAU,CACT,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,EACjE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAC7B;SACA,QAAQ,CACP,4JAA4J,CAC7J;IACH,WAAW,EAAE,CAAC;SACX,MAAM,EAAE;SACR,GAAG,EAAE;SACL,OAAO,CAAC,+BAA+B,CAAC;SACxC,QAAQ,CAAC,mBAAmB,CAAC;IAChC,WAAW,EAAE,CAAC;SACX,MAAM,EAAE;SACR,GAAG,EAAE;SACL,OAAO,CAAC,kCAAkC,CAAC;SAC3C,QAAQ,CAAC,mBAAmB,CAAC;IAChC,aAAa,EAAE,CAAC;SACb,MAAM,EAAE;SACR,GAAG,EAAE;SACL,OAAO,CAAC,8BAA8B,CAAC;SACvC,QAAQ,CAAC,qBAAqB,CAAC;IAClC,mEAAmE;IACnE,6EAA6E;IAC7E,gFAAgF;IAChF,eAAe,EAAE,CAAC;SACf,UAAU,CACT,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,EACjE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,4BAA4B,CAAC,CACvD;SACA,QAAQ,CAAC,yEAAyE,CAAC;CACvF,CAAC,CAAC;AAIH,IAAI,OAAiC,CAAC;AAEtC,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,cAAc,CAAC,kBAAkB,EAAE;QAC7C,YAAY,EAAE,gBAAgB;QAC9B,WAAW,EAAE,mBAAmB;QAChC,WAAW,EAAE,mBAAmB;QAChC,aAAa,EAAE,qBAAqB;QACpC,eAAe,EAAE,uBAAuB;KACzC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,YAAY,EAAE,CAAC;SACZ,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,EAAE;SACV,QAAQ,CACP,4JAA4J,CAC7J;IACH,WAAW,EAAE,CAAC;SACX,MAAM,EAAE;SACR,GAAG,EAAE;SACL,OAAO,CAAC,+BAA+B,CAAC;SACxC,QAAQ,CAAC,mBAAmB,CAAC;IAChC,WAAW,EAAE,CAAC;SACX,MAAM,EAAE;SACR,GAAG,EAAE;SACL,OAAO,CAAC,kCAAkC,CAAC;SAC3C,QAAQ,CAAC,mBAAmB,CAAC;IAChC,aAAa,EAAE,CAAC;SACb,MAAM,EAAE;SACR,GAAG,EAAE;SACL,OAAO,CAAC,8BAA8B,CAAC;SACvC,QAAQ,CAAC,qBAAqB,CAAC;IAClC,eAAe,EAAE,CAAC;SACf,MAAM,EAAE;SACR,GAAG,EAAE;SACL,OAAO,CAAC,4BAA4B,CAAC;SACrC,QAAQ,CAAC,yEAAyE,CAAC;CACvF,CAAC,CAAC;AAIH,IAAI,OAAiC,CAAC;AAEtC,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,cAAc,CAAC,kBAAkB,EAAE;QAC7C,YAAY,EAAE,gBAAgB;QAC9B,WAAW,EAAE,mBAAmB;QAChC,WAAW,EAAE,mBAAmB;QAChC,aAAa,EAAE,qBAAqB;QACpC,eAAe,EAAE,uBAAuB;KACzC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC"}