@cyanheads/epa-mcp-server 0.1.1

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 (88) hide show
  1. package/CLAUDE.md +405 -0
  2. package/Dockerfile +98 -0
  3. package/README.md +322 -0
  4. package/changelog/0.1.x/0.1.0.md +22 -0
  5. package/changelog/0.1.x/0.1.1.md +18 -0
  6. package/changelog/template.md +119 -0
  7. package/dist/config/server-config.d.ts +15 -0
  8. package/dist/config/server-config.d.ts.map +1 -0
  9. package/dist/config/server-config.js +38 -0
  10. package/dist/config/server-config.js.map +1 -0
  11. package/dist/index.d.ts +7 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +28 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/mcp-server/resources/definitions/facility.resource.d.ts +9 -0
  16. package/dist/mcp-server/resources/definitions/facility.resource.d.ts.map +1 -0
  17. package/dist/mcp-server/resources/definitions/facility.resource.js +24 -0
  18. package/dist/mcp-server/resources/definitions/facility.resource.js.map +1 -0
  19. package/dist/mcp-server/resources/definitions/index.d.ts +12 -0
  20. package/dist/mcp-server/resources/definitions/index.d.ts.map +1 -0
  21. package/dist/mcp-server/resources/definitions/index.js +10 -0
  22. package/dist/mcp-server/resources/definitions/index.js.map +1 -0
  23. package/dist/mcp-server/resources/definitions/superfund-site.resource.d.ts +9 -0
  24. package/dist/mcp-server/resources/definitions/superfund-site.resource.d.ts.map +1 -0
  25. package/dist/mcp-server/resources/definitions/superfund-site.resource.js +26 -0
  26. package/dist/mcp-server/resources/definitions/superfund-site.resource.js.map +1 -0
  27. package/dist/mcp-server/tools/definitions/get-air-quality.tool.d.ts +46 -0
  28. package/dist/mcp-server/tools/definitions/get-air-quality.tool.d.ts.map +1 -0
  29. package/dist/mcp-server/tools/definitions/get-air-quality.tool.js +193 -0
  30. package/dist/mcp-server/tools/definitions/get-air-quality.tool.js.map +1 -0
  31. package/dist/mcp-server/tools/definitions/get-facility.tool.d.ts +61 -0
  32. package/dist/mcp-server/tools/definitions/get-facility.tool.d.ts.map +1 -0
  33. package/dist/mcp-server/tools/definitions/get-facility.tool.js +193 -0
  34. package/dist/mcp-server/tools/definitions/get-facility.tool.js.map +1 -0
  35. package/dist/mcp-server/tools/definitions/get-tri-releases.tool.d.ts +26 -0
  36. package/dist/mcp-server/tools/definitions/get-tri-releases.tool.d.ts.map +1 -0
  37. package/dist/mcp-server/tools/definitions/get-tri-releases.tool.js +92 -0
  38. package/dist/mcp-server/tools/definitions/get-tri-releases.tool.js.map +1 -0
  39. package/dist/mcp-server/tools/definitions/index.d.ts +305 -0
  40. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -0
  41. package/dist/mcp-server/tools/definitions/index.js +31 -0
  42. package/dist/mcp-server/tools/definitions/index.js.map +1 -0
  43. package/dist/mcp-server/tools/definitions/search-facilities.tool.d.ts +58 -0
  44. package/dist/mcp-server/tools/definitions/search-facilities.tool.d.ts.map +1 -0
  45. package/dist/mcp-server/tools/definitions/search-facilities.tool.js +182 -0
  46. package/dist/mcp-server/tools/definitions/search-facilities.tool.js.map +1 -0
  47. package/dist/mcp-server/tools/definitions/search-superfund.tool.d.ts +49 -0
  48. package/dist/mcp-server/tools/definitions/search-superfund.tool.d.ts.map +1 -0
  49. package/dist/mcp-server/tools/definitions/search-superfund.tool.js +155 -0
  50. package/dist/mcp-server/tools/definitions/search-superfund.tool.js.map +1 -0
  51. package/dist/mcp-server/tools/definitions/search-tri-releases.tool.d.ts +29 -0
  52. package/dist/mcp-server/tools/definitions/search-tri-releases.tool.d.ts.map +1 -0
  53. package/dist/mcp-server/tools/definitions/search-tri-releases.tool.js +113 -0
  54. package/dist/mcp-server/tools/definitions/search-tri-releases.tool.js.map +1 -0
  55. package/dist/mcp-server/tools/definitions/search-violations.tool.d.ts +48 -0
  56. package/dist/mcp-server/tools/definitions/search-violations.tool.d.ts.map +1 -0
  57. package/dist/mcp-server/tools/definitions/search-violations.tool.js +156 -0
  58. package/dist/mcp-server/tools/definitions/search-violations.tool.js.map +1 -0
  59. package/dist/mcp-server/tools/definitions/search-water-systems.tool.d.ts +38 -0
  60. package/dist/mcp-server/tools/definitions/search-water-systems.tool.d.ts.map +1 -0
  61. package/dist/mcp-server/tools/definitions/search-water-systems.tool.js +147 -0
  62. package/dist/mcp-server/tools/definitions/search-water-systems.tool.js.map +1 -0
  63. package/dist/services/airnow/airnow-service.d.ts +45 -0
  64. package/dist/services/airnow/airnow-service.d.ts.map +1 -0
  65. package/dist/services/airnow/airnow-service.js +160 -0
  66. package/dist/services/airnow/airnow-service.js.map +1 -0
  67. package/dist/services/airnow/types.d.ts +39 -0
  68. package/dist/services/airnow/types.d.ts.map +1 -0
  69. package/dist/services/airnow/types.js +6 -0
  70. package/dist/services/airnow/types.js.map +1 -0
  71. package/dist/services/dmap/dmap-service.d.ts +60 -0
  72. package/dist/services/dmap/dmap-service.d.ts.map +1 -0
  73. package/dist/services/dmap/dmap-service.js +320 -0
  74. package/dist/services/dmap/dmap-service.js.map +1 -0
  75. package/dist/services/dmap/types.d.ts +108 -0
  76. package/dist/services/dmap/types.d.ts.map +1 -0
  77. package/dist/services/dmap/types.js +6 -0
  78. package/dist/services/dmap/types.js.map +1 -0
  79. package/dist/services/echo/echo-service.d.ts +57 -0
  80. package/dist/services/echo/echo-service.d.ts.map +1 -0
  81. package/dist/services/echo/echo-service.js +295 -0
  82. package/dist/services/echo/echo-service.js.map +1 -0
  83. package/dist/services/echo/types.d.ts +198 -0
  84. package/dist/services/echo/types.d.ts.map +1 -0
  85. package/dist/services/echo/types.js +6 -0
  86. package/dist/services/echo/types.js.map +1 -0
  87. package/package.json +80 -0
  88. package/server.json +111 -0
package/README.md ADDED
@@ -0,0 +1,322 @@
1
+ <div align="center">
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, and real-time air quality (AirNow) via MCP. STDIO or Streamable HTTP.</b>
4
+ <div>8 Tools • 2 Resources</div>
5
+ </p>
6
+ </div>
7
+
8
+ <div align="center">
9
+
10
+ [![Version](https://img.shields.io/badge/Version-0.1.1-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.29.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![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.2-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
+
12
+ </div>
13
+
14
+ <div align="center">
15
+
16
+ [![Framework](https://img.shields.io/badge/Built%20on-@cyanheads/mcp--ts--core-67E8F9?style=flat-square)](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
17
+
18
+ </div>
19
+
20
+ ---
21
+
22
+ ## Tools
23
+
24
+ 8 tools spanning EPA facility compliance, toxic chemical releases, Superfund cleanup sites, drinking water safety, and real-time air quality:
25
+
26
+ | Tool | Description |
27
+ |:---|:---|
28
+ | `epa_search_facilities` | Search EPA-regulated facilities by location, 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. |
29
+ | `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. |
30
+ | `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. |
31
+ | `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. |
32
+ | `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. |
33
+ | `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. |
34
+ | `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. |
35
+ | `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. |
36
+
37
+ ### `epa_search_facilities`
38
+
39
+ Search for EPA-regulated facilities with cross-program compliance data.
40
+
41
+ - Geographic filters: ZIP code, state, city (city requires state)
42
+ - Program filter: narrow to CAA, CWA, RCRA, TRI, or SDWA registrants
43
+ - Compliance filter: `has_violation` flag to surface only non-compliant facilities
44
+ - Returns `RegistryID` (key for `epa_get_facility`), `FacFIPSCode` (county FIPS for Census chaining), and coordinates
45
+ - Results cap enforced — unscoped searches are prohibited at the input validation layer
46
+
47
+ ---
48
+
49
+ ### `epa_get_facility`
50
+
51
+ Retrieve a comprehensive compliance profile by EPA Registry ID.
52
+
53
+ - 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)
54
+ - Uses `Promise.allSettled` — partial data returned even if one upstream endpoint fails
55
+ - Includes formal enforcement actions, penalty amounts, and inspection dates across all programs
56
+
57
+ ---
58
+
59
+ ### `epa_search_violations`
60
+
61
+ Search area-level EPA enforcement cases — distinct from per-facility history in `epa_get_facility`.
62
+
63
+ - Program filter: CAA, CWA, RCRA, SDWA, CERCLA, FIFRA, or TSCA
64
+ - Case type: civil, criminal, or all
65
+ - Date range filtering by filing date (ISO 8601)
66
+ - Returns case identifier, affected facility name and Registry ID for downstream `epa_get_facility` lookup
67
+
68
+ ---
69
+
70
+ ### `epa_get_air_quality`
71
+
72
+ Get current AQI observations or daily forecasts from AirNow.
73
+
74
+ - Accepts ZIP code or latitude/longitude coordinates
75
+ - `mode: current` returns the latest observed AQI per pollutant; `mode: forecast` returns daily AQI forecasts (requires `forecast_date`)
76
+ - AQI categories: Good (1) through Hazardous (6) with numeric and text category
77
+ - Data is preliminary — suitable for awareness, not regulatory or enforcement decisions
78
+ - AirNow responses cached at ~1 hour TTL to respect rate limits
79
+
80
+ ---
81
+
82
+ ### `epa_get_tri_releases`
83
+
84
+ Query per-chemical release breakdown for a single TRI facility.
85
+
86
+ - Accepts TRI facility ID from `epa_search_facilities` results
87
+ - Returns release quantities by medium: air, water, land, underground injection
88
+ - Optional year filter (defaults to most recent available); optional chemical name filter
89
+ - TRI data lags ~18 months — most recent available year is typically 2 years prior to current
90
+
91
+ ---
92
+
93
+ ### `epa_search_tri_releases`
94
+
95
+ Identify top polluters in a region via TRI data.
96
+
97
+ - State and optional county scope; required year parameter
98
+ - Optional chemical name filter to focus on a specific substance
99
+ - Returns facility coordinates for downstream map or proximity analysis
100
+ - Complement to `epa_get_tri_releases` — use this for area discovery, then drill into a specific facility
101
+
102
+ ---
103
+
104
+ ### `epa_search_superfund`
105
+
106
+ Search Superfund (CERCLA/SEMS) sites by location or proximity.
107
+
108
+ - Two input shapes: state/city/ZIP for administrative filters, or lat/lng + radius for proximity
109
+ - NPL status filter: listed, not-listed, proposed, or all
110
+ - Returns site cleanup status and coordinates for downstream spatial analysis
111
+
112
+ ---
113
+
114
+ ### `epa_search_water_systems`
115
+
116
+ Identify drinking water systems with active or recent violations.
117
+
118
+ - State and optional ZIP code scope
119
+ - `has_violation` flag surfaces only systems with current violations
120
+ - PWS type filter: community (`CWS`), non-transient non-community (`NTNCWS`), or transient non-community (`TNCWS`)
121
+
122
+ ## Resources and prompts
123
+
124
+ | Type | Name | Description |
125
+ |:---|:---|:---|
126
+ | Resource | `epa://facility/{registry_id}` | Full compliance profile for a facility by EPA Registry ID (same data as `epa_get_facility`) |
127
+ | Resource | `epa://superfund/{site_id}` | Superfund site record by SEMS site ID |
128
+
129
+ All resource data is also reachable via tools. Use `epa_get_facility` and `epa_search_superfund` for programmatic access in tool-only MCP clients.
130
+
131
+ ## Features
132
+
133
+ Built on [`@cyanheads/mcp-ts-core`](https://www.npmjs.com/package/@cyanheads/mcp-ts-core):
134
+
135
+ - Declarative tool and resource definitions — single file per primitive, framework handles registration and validation
136
+ - Unified error handling — handlers throw, framework catches, classifies, and formats
137
+ - Pluggable auth: `none`, `jwt`, `oauth`
138
+ - Swappable storage backends: `in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`
139
+ - Structured logging with optional OpenTelemetry tracing
140
+ - STDIO and Streamable HTTP transports
141
+
142
+ EPA-specific:
143
+
144
+ - Three complementary EPA APIs unified behind a single `epa_` tool surface: ECHO (facility compliance), Envirofacts DMAP (TRI, Superfund, SDWIS), and AirNow (real-time air quality)
145
+ - Parallel ECHO DFR aggregation in `epa_get_facility` — 3–5 upstream calls resolved concurrently with `Promise.allSettled`
146
+ - AirNow response caching (~1 hour TTL) to stay within per-key rate limits
147
+ - DMAP coordinate normalization — `tri.tri_facility` DDMMSS integers converted to decimal degrees
148
+
149
+ Agent-friendly output:
150
+
151
+ - Cross-tool join keys surfaced on every response — `RegistryID` and `FacFIPSCode` from facility search feed directly into compliance, TRI, and Census API workflows
152
+ - Typed enforcement and compliance status fields — agents branch on data values, not string parsing
153
+ - Structured partial failure — `epa_get_facility` returns available program data even when one DFR endpoint is unavailable, with per-section status
154
+
155
+ ## Getting started
156
+
157
+ Add the following to your MCP client configuration file. An AirNow API key is required for `epa_get_air_quality` — register free at [docs.airnowapi.org](https://docs.airnowapi.org/account/request/). ECHO and DMAP tools work without authentication.
158
+
159
+ ```json
160
+ {
161
+ "mcpServers": {
162
+ "epa": {
163
+ "type": "stdio",
164
+ "command": "bunx",
165
+ "args": ["@cyanheads/epa-mcp-server@latest"],
166
+ "env": {
167
+ "MCP_TRANSPORT_TYPE": "stdio",
168
+ "MCP_LOG_LEVEL": "info",
169
+ "AIRNOW_API_KEY": "your-airnow-key"
170
+ }
171
+ }
172
+ }
173
+ }
174
+ ```
175
+
176
+ Or with npx (no Bun required):
177
+
178
+ ```json
179
+ {
180
+ "mcpServers": {
181
+ "epa": {
182
+ "type": "stdio",
183
+ "command": "npx",
184
+ "args": ["-y", "@cyanheads/epa-mcp-server@latest"],
185
+ "env": {
186
+ "MCP_TRANSPORT_TYPE": "stdio",
187
+ "MCP_LOG_LEVEL": "info",
188
+ "AIRNOW_API_KEY": "your-airnow-key"
189
+ }
190
+ }
191
+ }
192
+ }
193
+ ```
194
+
195
+ For Streamable HTTP, set the transport and start the server:
196
+
197
+ ```sh
198
+ MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 AIRNOW_API_KEY=... bun run start:http
199
+ # Server listens at http://localhost:3010/mcp
200
+ ```
201
+
202
+ ### Prerequisites
203
+
204
+ - [Bun v1.3.0](https://bun.sh/) or higher (or Node.js v24+).
205
+ - An AirNow API key for `epa_get_air_quality` — register free at [docs.airnowapi.org/account/request](https://docs.airnowapi.org/account/request/). ECHO and DMAP tools require no API key.
206
+
207
+ ### Installation
208
+
209
+ 1. **Clone the repository:**
210
+
211
+ ```sh
212
+ git clone https://github.com/cyanheads/epa-mcp-server.git
213
+ ```
214
+
215
+ 2. **Navigate into the directory:**
216
+
217
+ ```sh
218
+ cd epa-mcp-server
219
+ ```
220
+
221
+ 3. **Install dependencies:**
222
+
223
+ ```sh
224
+ bun install
225
+ ```
226
+
227
+ 4. **Configure environment:**
228
+
229
+ ```sh
230
+ cp .env.example .env
231
+ # edit .env and set AIRNOW_API_KEY
232
+ ```
233
+
234
+ ## Configuration
235
+
236
+ All configuration is validated at startup via Zod schemas in `src/config/`. Key environment variables:
237
+
238
+ | Variable | Description | Default |
239
+ |:---|:---|:---|
240
+ | `AIRNOW_API_KEY` | **Required for `epa_get_air_quality`.** Free registration at [docs.airnowapi.org](https://docs.airnowapi.org/account/request/). ECHO and DMAP tools work without it. | — |
241
+ | `EPA_ECHO_BASE_URL` | ECHO API base URL | `https://echodata.epa.gov/echo` |
242
+ | `EPA_DMAP_BASE_URL` | Envirofacts DMAP API base URL | `https://data.epa.gov/dmapservice` |
243
+ | `EPA_AIRNOW_BASE_URL` | AirNow API base URL | `https://www.airnowapi.org/aq` |
244
+ | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http` | `stdio` |
245
+ | `MCP_HTTP_PORT` | HTTP server port | `3010` |
246
+ | `MCP_HTTP_ENDPOINT_PATH` | HTTP endpoint path | `/mcp` |
247
+ | `MCP_AUTH_MODE` | Auth mode: `none`, `jwt`, or `oauth` | `none` |
248
+ | `MCP_LOG_LEVEL` | Log level (RFC 5424) | `info` |
249
+ | `LOGS_DIR` | Directory for log files (Node.js only) | `<project-root>/logs` |
250
+ | `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1` | `in-memory` |
251
+ | `OTEL_ENABLED` | Enable [OpenTelemetry](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry) tracing and metrics | `false` |
252
+
253
+ See [`.env.example`](./.env.example) for the full list of optional overrides.
254
+
255
+ ## Running the server
256
+
257
+ ### Local development
258
+
259
+ - **Build and run:**
260
+
261
+ ```sh
262
+ # One-time build
263
+ bun run rebuild
264
+
265
+ # Run the built server
266
+ bun run start:stdio
267
+ # or
268
+ bun run start:http
269
+ ```
270
+
271
+ - **Run checks and tests:**
272
+
273
+ ```sh
274
+ bun run devcheck # Lint, format, typecheck, security
275
+ bun run test # Vitest test suite
276
+ bun run lint:mcp # Validate MCP definitions against spec
277
+ ```
278
+
279
+ ### Docker
280
+
281
+ ```sh
282
+ docker build -t epa-mcp-server .
283
+ docker run --rm -e AIRNOW_API_KEY=your-key -p 3010:3010 epa-mcp-server
284
+ ```
285
+
286
+ 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.
287
+
288
+ ## Project structure
289
+
290
+ | Directory | Purpose |
291
+ |:---|:---|
292
+ | `src/index.ts` | `createApp()` entry point — registers tools/resources and inits services. |
293
+ | `src/config` | Server-specific environment variable parsing and validation with Zod. |
294
+ | `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). Eight tools across ECHO, DMAP, and AirNow. |
295
+ | `src/mcp-server/resources` | Resource definitions (`*.resource.ts`). Facility and Superfund URI handlers. |
296
+ | `src/services/echo` | ECHO REST API service layer — facility search, facility detail, enforcement cases. |
297
+ | `src/services/dmap` | Envirofacts DMAP service layer — TRI releases, Superfund sites, drinking water systems. |
298
+ | `src/services/airnow` | AirNow service layer — current and forecast AQI observations. |
299
+ | `tests/` | Unit and integration tests mirroring `src/`. |
300
+
301
+ ## Development guide
302
+
303
+ See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
304
+
305
+ - Handlers throw, framework catches — no `try/catch` in tool logic
306
+ - Use `ctx.log` for request-scoped logging, `ctx.state` for tenant-scoped storage
307
+ - Register new tools and resources via the barrels in `src/mcp-server/*/definitions/index.ts`
308
+ - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
309
+ - ECHO searches must enforce at least one geographic parameter — unscoped queries time out against the live API
310
+
311
+ ## Contributing
312
+
313
+ Issues and pull requests are welcome. Run checks and tests before submitting:
314
+
315
+ ```sh
316
+ bun run devcheck
317
+ bun run test
318
+ ```
319
+
320
+ ## License
321
+
322
+ Apache-2.0 — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,22 @@
1
+ ---
2
+ summary: "Initial release — EPA environmental data server (ECHO, TRI, Superfund, SDWIS, AirNow)"
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.1.0 — 2026-05-25
8
+
9
+ ## Added
10
+
11
+ - **`epa_search_facilities`** — search EPA-regulated facilities by location, program, and compliance status via ECHO `get_facility_info`; returns `RegistryID`, county FIPS, coordinates, per-program flags, and TRI totals
12
+ - **`epa_get_facility`** — full compliance profile aggregated from 3–5 parallel ECHO DFR endpoints; uses `Promise.allSettled` for partial-success resilience
13
+ - **`epa_search_violations`** — area-level civil and criminal enforcement case search via ECHO `case_rest_services`
14
+ - **`epa_get_air_quality`** — current and forecast AQI by ZIP or lat/lng via AirNow; ~1 hour response cache
15
+ - **`epa_get_tri_releases`** — per-chemical release breakdown by medium for a single TRI facility via DMAP `tri.tri_reporting_form`
16
+ - **`epa_search_tri_releases`** — area TRI emitter search by state/county and year
17
+ - **`epa_search_superfund`** — Superfund site search by location or lat/lng + radius via DMAP `sems.envirofacts_site`
18
+ - **`epa_search_water_systems`** — drinking water system search by state/ZIP via DMAP `sdwis.water_system`
19
+ - **`epa://facility/{registry_id}`** resource — compliance profile by EPA Registry ID
20
+ - **`epa://superfund/{site_id}`** resource — Superfund site record by SEMS site ID
21
+ - `EchoService`, `DmapService`, `AirNowService` — service layers for ECHO, Envirofacts DMAP, and AirNow APIs
22
+ - `AIRNOW_API_KEY` env var (required for air quality tool); ECHO and DMAP are unauthenticated
@@ -0,0 +1,18 @@
1
+ ---
2
+ summary: "Bug fixes from field testing — TRI field mapping, Superfund site lookup, SDWIS query, AirNow cache keys, ECHO headers, and server.json env var config"
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.1.1 — 2026-05-25
8
+
9
+ ## Fixed
10
+
11
+ - **`epa_get_tri_releases` / `epa_search_tri_releases`** — corrected field name mappings from `tri_reporting_form`; chemical names and release quantities (air, water, land, underground injection) were always empty due to wrong column names ([#3](https://github.com/cyanheads/epa-mcp-server/issues/3))
12
+ - **`epa_search_superfund`** — `site_name` was always empty; fixed wrong field name in `normalizeSemsSite()` ([#4](https://github.com/cyanheads/epa-mcp-server/issues/4))
13
+ - **`epa://superfund/{site_id}` resource** — site lookup heuristic was wrong; resource never found sites by ID; fixed ID matching logic ([#6](https://github.com/cyanheads/epa-mcp-server/issues/6))
14
+ - **`epa_search_water_systems`** — `has_violation` filter crashed because `violation_flag` column does not exist in SDWIS; switched to correct column; city field was always missing and is now populated ([#5](https://github.com/cyanheads/epa-mcp-server/issues/5))
15
+ - **`epa_get_air_quality`** — cache key generation produced invalid characters (slashes from lat/lng), crashing every call; replaced with a safe `key-value` join ([#1](https://github.com/cyanheads/epa-mcp-server/issues/1))
16
+ - **`epa_search_facilities` / `epa_search_violations`** — ECHO API bot-blocking returned empty results silently; added `User-Agent` header to all ECHO requests ([#2](https://github.com/cyanheads/epa-mcp-server/issues/2))
17
+ - **`AIRNOW_API_KEY`** — env var was missing from `server.json` `environmentVariables` and `manifest.json` `user_config`; added to both ([#7](https://github.com/cyanheads/epa-mcp-server/issues/7))
18
+ - **`epa_get_facility`** — tool description no longer leaks internal "ECHO DFR endpoints in parallel" implementation detail ([#8](https://github.com/cyanheads/epa-mcp-server/issues/8))
@@ -0,0 +1,119 @@
1
+ ---
2
+ # FORMAT REFERENCE — do not edit. Copy this file to
3
+ # `changelog/<major.minor>.x/<version>.md` (e.g. `changelog/0.8.x/0.8.6.md`)
4
+ # to author a new release. Set that file's H1 to `# <version> — YYYY-MM-DD`
5
+ # with a concrete date.
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.
11
+ summary: ""
12
+
13
+ # Set `true` when consumers must change code to upgrade: API removals,
14
+ # signature changes, config renames, behavior changes that break existing
15
+ # usage. Flagged as `Breaking` in the rollup.
16
+ breaking: false
17
+
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.
21
+ security: false
22
+ ---
23
+
24
+ # <version> — YYYY-MM-DD
25
+
26
+ <!--
27
+ AUTHORING GUIDE — applies to the new per-version file you create from this
28
+ template.
29
+
30
+ Audience: someone scanning release notes to decide what affects them. Lead
31
+ each bullet with the symbol or concept name in **bold** so they can skip
32
+ what's irrelevant and zoom in on what's not.
33
+
34
+ Tone: terse, fact-dense, not verbose. Default to one sentence per bullet —
35
+ name the symbol, state what changed, stop. Use a second sentence only when
36
+ it carries weight. If a bullet feels long, it is.
37
+
38
+ Cut: mechanism walkthroughs (those belong in JSDoc, AGENTS.md, or the
39
+ relevant skill), ceremonial framings ("This release introduces…",
40
+ backwards-compat paragraphs), file-by-file test enumerations, internal
41
+ implementation notes. Prefer code/symbol names over English re-explanations.
42
+
43
+ Narrative intro: skip by default. Add one short sentence only when the
44
+ release theme genuinely needs framing the bullets can't carry.
45
+
46
+ Sections: Keep a Changelog order — Added, Changed, Deprecated, Removed,
47
+ Fixed, Security. Include only sections with entries; delete the rest
48
+ (including the commented-out scaffolding below). Don't ship empty headers.
49
+
50
+ Include: every distinct fact a reader needs to adopt or audit the release —
51
+ new exports, signatures, lint rule IDs, env vars, breaking changes, version
52
+ bumps on shipped skills. Nothing more.
53
+
54
+ Links: link issues, PRs, docs, or skills where they help a reader jump to
55
+ context. Once per item per entry — don't re-link the same issue in summary,
56
+ narrative, and bullet. Skip links for inline symbol names; code spans speak
57
+ for themselves.
58
+
59
+ Issue/PR URLs: use full URLs. GitHub's bare `#NN` auto-link only resolves
60
+ inside its own UI, not in npm reads or local editors.
61
+
62
+ [#38](https://github.com/cyanheads/mcp-ts-core/issues/38) ← issue
63
+ [#42](https://github.com/cyanheads/mcp-ts-core/pull/42) ← PR
64
+
65
+ Verify numbers exist before linking (`gh issue view NN`, `gh pr view NN`).
66
+ Never speculate on a future number — `#42` for an upcoming PR silently
67
+ resolves to whatever real item already owns 42, and timeline previews pull
68
+ in that unrelated item's metadata.
69
+
70
+ TAG ANNOTATIONS — the annotated tag body renders as the GitHub Release body
71
+ via `gh release create --notes-from-tag`. The tag is a derivative of this
72
+ changelog entry — a condensed, scannable version, not a copy. Format:
73
+
74
+ <theme — omit version number, GitHub prepends it>
75
+ ← blank line
76
+ <1-2 sentence context: what this release does>
77
+ ← blank line
78
+ Dependency bumps: ← section header
79
+ ← blank line
80
+ - `@cyanheads/mcp-ts-core` ^0.9.1 → ^0.9.6 ← bullet
81
+ ← blank line
82
+ Changed: ← only sections with entries
83
+ ← blank line
84
+ - `format()` output includes `query` in text mode
85
+ ← blank line
86
+ Added:
87
+ ← blank line
88
+ - `manifest.json` scaffolded for MCPB bundle support
89
+ - Install badges (Claude Desktop, Cursor, VS Code)
90
+ ← blank line
91
+ <N> tests pass; `bun run devcheck` clean. ← footer
92
+
93
+ Never a flat comma-separated string. Always structured markdown with
94
+ sections. The tag must scan well as a rendered GitHub Release page.
95
+ -->
96
+
97
+ ## Added
98
+
99
+ -
100
+
101
+ ## Changed
102
+
103
+ -
104
+
105
+ <!-- ## Deprecated
106
+
107
+ - -->
108
+
109
+ <!-- ## Removed
110
+
111
+ - -->
112
+
113
+ ## Fixed
114
+
115
+ -
116
+
117
+ <!-- ## Security
118
+
119
+ - -->
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @fileoverview Server-specific environment variable configuration for epa-mcp-server.
3
+ * @module config/server-config
4
+ */
5
+ import { z } from '@cyanheads/mcp-ts-core';
6
+ declare const ServerConfigSchema: z.ZodObject<{
7
+ airNowApiKey: z.ZodString;
8
+ echoBaseUrl: z.ZodDefault<z.ZodString>;
9
+ dmapBaseUrl: z.ZodDefault<z.ZodString>;
10
+ airNowBaseUrl: z.ZodDefault<z.ZodString>;
11
+ }, z.core.$strip>;
12
+ export type ServerConfig = z.infer<typeof ServerConfigSchema>;
13
+ export declare function getServerConfig(): ServerConfig;
14
+ export {};
15
+ //# sourceMappingURL=server-config.d.ts.map
@@ -0,0 +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;;;;;iBAoBtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CAQ9C"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @fileoverview Server-specific environment variable configuration for epa-mcp-server.
3
+ * @module config/server-config
4
+ */
5
+ import { z } from '@cyanheads/mcp-ts-core';
6
+ import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
7
+ const ServerConfigSchema = z.object({
8
+ airNowApiKey: z
9
+ .string()
10
+ .min(1)
11
+ .describe('AirNow API key (free registration at docs.airnowapi.org)'),
12
+ echoBaseUrl: z
13
+ .string()
14
+ .url()
15
+ .default('https://echodata.epa.gov/echo')
16
+ .describe('ECHO API base URL'),
17
+ dmapBaseUrl: z
18
+ .string()
19
+ .url()
20
+ .default('https://data.epa.gov/dmapservice')
21
+ .describe('DMAP API base URL'),
22
+ airNowBaseUrl: z
23
+ .string()
24
+ .url()
25
+ .default('https://www.airnowapi.org/aq')
26
+ .describe('AirNow API base URL'),
27
+ });
28
+ let _config;
29
+ export function getServerConfig() {
30
+ _config ??= parseEnvConfig(ServerConfigSchema, {
31
+ airNowApiKey: 'AIRNOW_API_KEY',
32
+ echoBaseUrl: 'EPA_ECHO_BASE_URL',
33
+ dmapBaseUrl: 'EPA_DMAP_BASE_URL',
34
+ airNowBaseUrl: 'EPA_AIRNOW_BASE_URL',
35
+ });
36
+ return _config;
37
+ }
38
+ //# sourceMappingURL=server-config.js.map
@@ -0,0 +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,YAAY,EAAE,CAAC;SACZ,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,CAAC,0DAA0D,CAAC;IACvE,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;CACnC,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;KACrC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @fileoverview epa-mcp-server MCP server entry point.
4
+ * @module index
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG"}
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @fileoverview epa-mcp-server MCP server entry point.
4
+ * @module index
5
+ */
6
+ import { createApp } from '@cyanheads/mcp-ts-core';
7
+ import { allResourceDefinitions } from './mcp-server/resources/definitions/index.js';
8
+ import { allToolDefinitions } from './mcp-server/tools/definitions/index.js';
9
+ import { initAirNowService } from './services/airnow/airnow-service.js';
10
+ import { initDmapService } from './services/dmap/dmap-service.js';
11
+ import { initEchoService } from './services/echo/echo-service.js';
12
+ await createApp({
13
+ tools: [...allToolDefinitions],
14
+ resources: [...allResourceDefinitions],
15
+ prompts: [],
16
+ instructions: 'EPA environmental data server covering facility compliance (ECHO), toxic releases (TRI), Superfund sites, drinking water systems (SDWIS), and real-time air quality (AirNow).\n' +
17
+ '- All tools are read-only. All APIs are US federal government public domain data.\n' +
18
+ '- ECHO tools require at least one geographic filter (zip_code, state, or city) — unscoped searches time out.\n' +
19
+ '- TRI data lags ~18 months — most recent available year is typically 2 years prior to current.\n' +
20
+ '- AirNow data is preliminary and not valid for regulatory, trend, or enforcement purposes.\n' +
21
+ '- Typical workflows: epa_search_facilities → epa_get_facility → epa_search_violations (compliance audit); epa_search_superfund (proximity); epa_search_water_systems + has_violation=true (drinking water safety).',
22
+ setup(core) {
23
+ initEchoService(core.config, core.storage);
24
+ initDmapService(core.config, core.storage);
25
+ initAirNowService(core.config, core.storage);
26
+ },
27
+ });
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAElE,MAAM,SAAS,CAAC;IACd,KAAK,EAAE,CAAC,GAAG,kBAAkB,CAAC;IAC9B,SAAS,EAAE,CAAC,GAAG,sBAAsB,CAAC;IACtC,OAAO,EAAE,EAAE;IACX,YAAY,EACV,iLAAiL;QACjL,qFAAqF;QACrF,gHAAgH;QAChH,kGAAkG;QAClG,8FAA8F;QAC9F,oNAAoN;IACtN,KAAK,CAAC,IAAI;QACR,eAAe,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3C,eAAe,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3C,iBAAiB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/C,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @fileoverview Resource for fetching a full EPA facility compliance profile by Registry ID.
3
+ * @module mcp-server/resources/definitions/facility.resource
4
+ */
5
+ import { z } from '@cyanheads/mcp-ts-core';
6
+ export declare const facilityResource: import("@cyanheads/mcp-ts-core").ResourceDefinition<z.ZodObject<{
7
+ registry_id: z.ZodString;
8
+ }, z.core.$strip>, undefined, undefined>;
9
+ //# sourceMappingURL=facility.resource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"facility.resource.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/facility.resource.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAY,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAIrD,eAAO,MAAM,gBAAgB;;wCAuB3B,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @fileoverview Resource for fetching a full EPA facility compliance profile by Registry ID.
3
+ * @module mcp-server/resources/definitions/facility.resource
4
+ */
5
+ import { resource, z } from '@cyanheads/mcp-ts-core';
6
+ import { notFound } from '@cyanheads/mcp-ts-core/errors';
7
+ import { getEchoService } from '../../../services/echo/echo-service.js';
8
+ export const facilityResource = resource('epa://facility/{registry_id}', {
9
+ name: 'epa-facility',
10
+ description: 'Full compliance profile for an EPA-regulated facility by EPA FRS Registry ID. Returns the same data as epa_get_facility.',
11
+ mimeType: 'application/json',
12
+ params: z.object({
13
+ registry_id: z.string().describe('EPA FRS Registry ID for the facility'),
14
+ }),
15
+ async handler(params, ctx) {
16
+ ctx.log.info('epa://facility resource', { registryId: params.registry_id });
17
+ const profile = await getEchoService().getFacility(params.registry_id, ctx);
18
+ if (!profile.registryId) {
19
+ throw notFound(`No facility found for Registry ID "${params.registry_id}". Use epa_search_facilities to discover valid Registry IDs.`, { registryId: params.registry_id });
20
+ }
21
+ return profile;
22
+ },
23
+ });
24
+ //# sourceMappingURL=facility.resource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"facility.resource.js","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/facility.resource.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEjE,MAAM,CAAC,MAAM,gBAAgB,GAAG,QAAQ,CAAC,8BAA8B,EAAE;IACvE,IAAI,EAAE,cAAc;IACpB,WAAW,EACT,0HAA0H;IAC5H,QAAQ,EAAE,kBAAkB;IAC5B,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,sCAAsC,CAAC;KACzE,CAAC;IAEF,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG;QACvB,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,yBAAyB,EAAE,EAAE,UAAU,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;QAE5E,MAAM,OAAO,GAAG,MAAM,cAAc,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;QAE5E,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;YACxB,MAAM,QAAQ,CACZ,sCAAsC,MAAM,CAAC,WAAW,8DAA8D,EACtH,EAAE,UAAU,EAAE,MAAM,CAAC,WAAW,EAAE,CACnC,CAAC;QACJ,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @fileoverview Barrel export for all EPA MCP server resource definitions.
3
+ * @module mcp-server/resources/definitions/index
4
+ */
5
+ export { facilityResource } from './facility.resource.js';
6
+ export { superfundSiteResource } from './superfund-site.resource.js';
7
+ export declare const allResourceDefinitions: readonly [import("@cyanheads/mcp-ts-core").ResourceDefinition<import("zod").ZodObject<{
8
+ registry_id: import("zod").ZodString;
9
+ }, import("zod/v4/core").$strip>, undefined, undefined>, import("@cyanheads/mcp-ts-core").ResourceDefinition<import("zod").ZodObject<{
10
+ site_id: import("zod").ZodString;
11
+ }, import("zod/v4/core").$strip>, undefined, undefined>];
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAKrE,eAAO,MAAM,sBAAsB;;;;wDAAqD,CAAC"}