@cyanheads/epa-mcp-server 0.3.2 → 0.4.0
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.
- package/AGENTS.md +13 -10
- package/CLAUDE.md +13 -10
- package/Dockerfile +108 -37
- package/README.md +67 -44
- package/changelog/0.3.x/0.3.3.md +34 -0
- package/changelog/0.4.x/0.4.0.md +35 -0
- package/dist/mcp-server/tools/definitions/get-air-quality.tool.d.ts +23 -9
- package/dist/mcp-server/tools/definitions/get-air-quality.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-air-quality.tool.js +166 -98
- package/dist/mcp-server/tools/definitions/get-air-quality.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-ejscreen.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-ejscreen.tool.js +1 -1
- package/dist/mcp-server/tools/definitions/get-ejscreen.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-facility.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-facility.tool.js +5 -14
- package/dist/mcp-server/tools/definitions/get-facility.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-tri-releases.tool.d.ts +2 -1
- package/dist/mcp-server/tools/definitions/get-tri-releases.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-tri-releases.tool.js +10 -14
- package/dist/mcp-server/tools/definitions/get-tri-releases.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/index.d.ts +51 -18
- package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-facilities.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-facilities.tool.js +9 -16
- package/dist/mcp-server/tools/definitions/search-facilities.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-superfund.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-superfund.tool.js +4 -14
- package/dist/mcp-server/tools/definitions/search-superfund.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-tri-releases.tool.d.ts +10 -3
- package/dist/mcp-server/tools/definitions/search-tri-releases.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-tri-releases.tool.js +50 -10
- package/dist/mcp-server/tools/definitions/search-tri-releases.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-violations.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-violations.tool.js +1 -3
- package/dist/mcp-server/tools/definitions/search-violations.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-water-systems.tool.d.ts +3 -1
- package/dist/mcp-server/tools/definitions/search-water-systems.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-water-systems.tool.js +23 -10
- package/dist/mcp-server/tools/definitions/search-water-systems.tool.js.map +1 -1
- package/dist/mcp-server/tools/format-lbs.d.ts +14 -0
- package/dist/mcp-server/tools/format-lbs.d.ts.map +1 -0
- package/dist/mcp-server/tools/format-lbs.js +16 -0
- package/dist/mcp-server/tools/format-lbs.js.map +1 -0
- package/dist/mcp-server/tools/format-location.d.ts +18 -0
- package/dist/mcp-server/tools/format-location.d.ts.map +1 -0
- package/dist/mcp-server/tools/format-location.js +26 -0
- package/dist/mcp-server/tools/format-location.js.map +1 -0
- package/dist/mcp-server/tools/format-tri-release.d.ts +16 -0
- package/dist/mcp-server/tools/format-tri-release.d.ts.map +1 -0
- package/dist/mcp-server/tools/format-tri-release.js +23 -0
- package/dist/mcp-server/tools/format-tri-release.js.map +1 -0
- package/dist/services/airnow/airnow-service.d.ts +10 -29
- package/dist/services/airnow/airnow-service.d.ts.map +1 -1
- package/dist/services/airnow/airnow-service.js +148 -95
- package/dist/services/airnow/airnow-service.js.map +1 -1
- package/dist/services/airnow/types.d.ts +62 -23
- package/dist/services/airnow/types.d.ts.map +1 -1
- package/dist/services/airnow/types.js +2 -1
- package/dist/services/airnow/types.js.map +1 -1
- package/dist/services/dmap/dmap-service.d.ts +41 -2
- package/dist/services/dmap/dmap-service.d.ts.map +1 -1
- package/dist/services/dmap/dmap-service.js +231 -116
- package/dist/services/dmap/dmap-service.js.map +1 -1
- package/dist/services/dmap/types.d.ts +17 -4
- package/dist/services/dmap/types.d.ts.map +1 -1
- package/dist/services/echo/echo-service.d.ts.map +1 -1
- package/dist/services/echo/echo-service.js +8 -5
- package/dist/services/echo/echo-service.js.map +1 -1
- package/dist/services/ejscreen/ejscreen-service.js +1 -1
- package/dist/services/ejscreen/ejscreen-service.js.map +1 -1
- package/package.json +11 -10
- package/server.json +16 -24
package/AGENTS.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** epa-mcp-server
|
|
4
|
-
**Version:** 0.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.4.0
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.14`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
|
-
**MCP SDK:** `@modelcontextprotocol/server` ^2.
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.2.0
|
|
8
8
|
**Zod:** ^4.6.5
|
|
9
9
|
|
|
10
10
|
> **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
|
|
@@ -54,7 +54,7 @@ export const searchItems = tool('search_items', {
|
|
|
54
54
|
annotations: { readOnlyHint: true },
|
|
55
55
|
input: z.object({
|
|
56
56
|
query: z.string().describe('Search terms'),
|
|
57
|
-
limit: z.number().default(10).describe('Max results'),
|
|
57
|
+
limit: z.number().int().min(1).max(100).default(10).describe('Max results (1–100)'),
|
|
58
58
|
}),
|
|
59
59
|
output: z.object({
|
|
60
60
|
items: z.array(z.object({
|
|
@@ -173,13 +173,14 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
173
173
|
| Property | Description |
|
|
174
174
|
|:---------|:------------|
|
|
175
175
|
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
|
|
176
|
-
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
|
|
176
|
+
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). |
|
|
177
177
|
| `ctx.requestInput` | Suspend and ask the caller for more input — `return ctx.requestInput({ inputRequests: { key: inputRequired.elicit({ message, requestedSchema }) } })`. Never returns; the handler is re-entered with the answers. Always present. |
|
|
178
178
|
| `ctx.inputs` | Reader over a retried request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped`. Empty on the first round. |
|
|
179
|
+
| `ctx.clientCapabilities` | Capabilities the client declared for this request, or `undefined` when unavailable. Use to choose optional input requests, never to skip consent. |
|
|
179
180
|
| `ctx.enrich` | Success-path agent context (empty-result notices, query echo, pagination totals) — `ctx.enrich(...)` or `.notice()` / `.total()` / `.echo()` / `.truncated()`. Reaches `structuredContent` and `content[]`; lands only when the definition declares an `enrichment` block (no-op otherwise). |
|
|
180
181
|
| `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
|
|
181
182
|
| `ctx.signal` | `AbortSignal` for cancellation. |
|
|
182
|
-
| `ctx.requestId` |
|
|
183
|
+
| `ctx.requestId` | Framework-generated request ID, also returned as `data.requestId` on error envelopes; client JSON-RPC ids are logged separately as `jsonRpcId`. |
|
|
183
184
|
| `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
|
|
184
185
|
|
|
185
186
|
---
|
|
@@ -188,7 +189,7 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
188
189
|
|
|
189
190
|
Handlers throw — the framework catches, classifies, and formats.
|
|
190
191
|
|
|
191
|
-
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. `recovery` is required (≥ 5 words, lint-validated).
|
|
192
|
+
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. `recovery` is required (≥ 5 words, lint-validated). The framework fills the declared hint when the failure carries its `reason` and no recovery hint; both client surfaces receive it. Error envelopes carry the generated `data.requestId`. Use explicit `{ recovery: { hint } }` for runtime guidance. Mark reasons produced below the handler with `thrownBy: 'service'`; remove unused entries. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) need no declaration.
|
|
192
193
|
|
|
193
194
|
```ts
|
|
194
195
|
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
@@ -200,7 +201,7 @@ errors: [
|
|
|
200
201
|
],
|
|
201
202
|
async handler(input, ctx) {
|
|
202
203
|
const item = await db.find(input.id);
|
|
203
|
-
if (!item) throw ctx.fail('no_match', `No item ${input.id}
|
|
204
|
+
if (!item) throw ctx.fail('no_match', `No item ${input.id}`);
|
|
204
205
|
return item;
|
|
205
206
|
}
|
|
206
207
|
```
|
|
@@ -221,7 +222,7 @@ throw new Error('Invalid query format'); // → ValidationError
|
|
|
221
222
|
|
|
222
223
|
// McpError — when no factory exists for the code
|
|
223
224
|
import { McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
224
|
-
throw new McpError(JsonRpcErrorCode.
|
|
225
|
+
throw new McpError(JsonRpcErrorCode.InitializationFailed, 'Connection failed', { pool: 'primary' });
|
|
225
226
|
```
|
|
226
227
|
|
|
227
228
|
See framework CLAUDE.md and the `api-errors` skill for the full auto-classification table, all available factories, and the contract reference.
|
|
@@ -336,6 +337,8 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
336
337
|
| `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
|
|
337
338
|
| `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
|
|
338
339
|
|
|
340
|
+
**CI is one file.** `.github/workflows/codeql.yml` is the only GitHub Actions workflow; verification gates run locally.
|
|
341
|
+
|
|
339
342
|
---
|
|
340
343
|
|
|
341
344
|
## Bundling
|
|
@@ -377,7 +380,7 @@ security: false # optional — true ONLY for a source
|
|
|
377
380
|
|
|
378
381
|
## Publishing
|
|
379
382
|
|
|
380
|
-
**Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the
|
|
383
|
+
**Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the release digest: theme line, `## Changes`, `## Gates`, changelog link last); `release-and-publish` then fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. A caller's brief may run a given release as `gated` instead — a `release-pr-review` pass on the open PR before `release-and-publish`. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history.
|
|
381
384
|
|
|
382
385
|
---
|
|
383
386
|
|
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** epa-mcp-server
|
|
4
|
-
**Version:** 0.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.4.0
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.14`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
|
-
**MCP SDK:** `@modelcontextprotocol/server` ^2.
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.2.0
|
|
8
8
|
**Zod:** ^4.6.5
|
|
9
9
|
|
|
10
10
|
> **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
|
|
@@ -54,7 +54,7 @@ export const searchItems = tool('search_items', {
|
|
|
54
54
|
annotations: { readOnlyHint: true },
|
|
55
55
|
input: z.object({
|
|
56
56
|
query: z.string().describe('Search terms'),
|
|
57
|
-
limit: z.number().default(10).describe('Max results'),
|
|
57
|
+
limit: z.number().int().min(1).max(100).default(10).describe('Max results (1–100)'),
|
|
58
58
|
}),
|
|
59
59
|
output: z.object({
|
|
60
60
|
items: z.array(z.object({
|
|
@@ -173,13 +173,14 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
173
173
|
| Property | Description |
|
|
174
174
|
|:---------|:------------|
|
|
175
175
|
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
|
|
176
|
-
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
|
|
176
|
+
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). |
|
|
177
177
|
| `ctx.requestInput` | Suspend and ask the caller for more input — `return ctx.requestInput({ inputRequests: { key: inputRequired.elicit({ message, requestedSchema }) } })`. Never returns; the handler is re-entered with the answers. Always present. |
|
|
178
178
|
| `ctx.inputs` | Reader over a retried request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped`. Empty on the first round. |
|
|
179
|
+
| `ctx.clientCapabilities` | Capabilities the client declared for this request, or `undefined` when unavailable. Use to choose optional input requests, never to skip consent. |
|
|
179
180
|
| `ctx.enrich` | Success-path agent context (empty-result notices, query echo, pagination totals) — `ctx.enrich(...)` or `.notice()` / `.total()` / `.echo()` / `.truncated()`. Reaches `structuredContent` and `content[]`; lands only when the definition declares an `enrichment` block (no-op otherwise). |
|
|
180
181
|
| `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
|
|
181
182
|
| `ctx.signal` | `AbortSignal` for cancellation. |
|
|
182
|
-
| `ctx.requestId` |
|
|
183
|
+
| `ctx.requestId` | Framework-generated request ID, also returned as `data.requestId` on error envelopes; client JSON-RPC ids are logged separately as `jsonRpcId`. |
|
|
183
184
|
| `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
|
|
184
185
|
|
|
185
186
|
---
|
|
@@ -188,7 +189,7 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
188
189
|
|
|
189
190
|
Handlers throw — the framework catches, classifies, and formats.
|
|
190
191
|
|
|
191
|
-
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. `recovery` is required (≥ 5 words, lint-validated).
|
|
192
|
+
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. `recovery` is required (≥ 5 words, lint-validated). The framework fills the declared hint when the failure carries its `reason` and no recovery hint; both client surfaces receive it. Error envelopes carry the generated `data.requestId`. Use explicit `{ recovery: { hint } }` for runtime guidance. Mark reasons produced below the handler with `thrownBy: 'service'`; remove unused entries. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) need no declaration.
|
|
192
193
|
|
|
193
194
|
```ts
|
|
194
195
|
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
@@ -200,7 +201,7 @@ errors: [
|
|
|
200
201
|
],
|
|
201
202
|
async handler(input, ctx) {
|
|
202
203
|
const item = await db.find(input.id);
|
|
203
|
-
if (!item) throw ctx.fail('no_match', `No item ${input.id}
|
|
204
|
+
if (!item) throw ctx.fail('no_match', `No item ${input.id}`);
|
|
204
205
|
return item;
|
|
205
206
|
}
|
|
206
207
|
```
|
|
@@ -221,7 +222,7 @@ throw new Error('Invalid query format'); // → ValidationError
|
|
|
221
222
|
|
|
222
223
|
// McpError — when no factory exists for the code
|
|
223
224
|
import { McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
224
|
-
throw new McpError(JsonRpcErrorCode.
|
|
225
|
+
throw new McpError(JsonRpcErrorCode.InitializationFailed, 'Connection failed', { pool: 'primary' });
|
|
225
226
|
```
|
|
226
227
|
|
|
227
228
|
See framework CLAUDE.md and the `api-errors` skill for the full auto-classification table, all available factories, and the contract reference.
|
|
@@ -336,6 +337,8 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
336
337
|
| `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
|
|
337
338
|
| `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
|
|
338
339
|
|
|
340
|
+
**CI is one file.** `.github/workflows/codeql.yml` is the only GitHub Actions workflow; verification gates run locally.
|
|
341
|
+
|
|
339
342
|
---
|
|
340
343
|
|
|
341
344
|
## Bundling
|
|
@@ -377,7 +380,7 @@ security: false # optional — true ONLY for a source
|
|
|
377
380
|
|
|
378
381
|
## Publishing
|
|
379
382
|
|
|
380
|
-
**Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the
|
|
383
|
+
**Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the release digest: theme line, `## Changes`, `## Gates`, changelog link last); `release-and-publish` then fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. A caller's brief may run a given release as `gated` instead — a `release-pr-review` pass on the open PR before `release-and-publish`. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history.
|
|
381
384
|
|
|
382
385
|
---
|
|
383
386
|
|
package/Dockerfile
CHANGED
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
# This stage installs all dependencies (including dev), builds the TypeScript
|
|
5
5
|
# source code into JavaScript, and prepares the production assets.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# Pinned to $BUILDPLATFORM rather than the target platform: `bun run build` emits
|
|
8
|
+
# JavaScript, and only `dist/` crosses into the production stage. Built for the
|
|
9
|
+
# target instead, the non-native leg of a `--platform linux/amd64,linux/arm64`
|
|
10
|
+
# build runs under QEMU, where bun >= 1.4 aborts with a JavaScriptCore allocator
|
|
11
|
+
# assertion and fails the multi-arch push.
|
|
12
|
+
#
|
|
13
|
+
# The constraint this assumes: the build stage produces platform-independent
|
|
14
|
+
# output. A stage that compiles a native addon needs the target-arch toolchain
|
|
15
|
+
# and cannot cross-compile this way — drop the flag there.
|
|
10
16
|
# ==============================================================================
|
|
11
|
-
FROM --platform=$BUILDPLATFORM oven/bun:1.4.
|
|
17
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS build
|
|
12
18
|
|
|
13
19
|
WORKDIR /usr/src/app
|
|
14
20
|
|
|
@@ -27,19 +33,92 @@ COPY . .
|
|
|
27
33
|
RUN bun run build
|
|
28
34
|
|
|
29
35
|
|
|
36
|
+
# ==============================================================================
|
|
37
|
+
# Production Dependencies Stage
|
|
38
|
+
#
|
|
39
|
+
# Installs the production dependency tree for the target platform. Every step
|
|
40
|
+
# here can run JavaScript — bunfig.toml's security scanner runs as a Bun
|
|
41
|
+
# program, and so do the OTel and musl-prune scripts — so the stage runs on
|
|
42
|
+
# $BUILDPLATFORM and cross-installs with `--os`/`--cpu`, which pick each
|
|
43
|
+
# platform-specific optional dependency (native bindings such as DuckDB's) for
|
|
44
|
+
# the target. Only `node_modules` leaves this stage.
|
|
45
|
+
#
|
|
46
|
+
# A clean image rather than `FROM build`: the build stage's node_modules holds
|
|
47
|
+
# devDependencies.
|
|
48
|
+
# ==============================================================================
|
|
49
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS deps
|
|
50
|
+
|
|
51
|
+
WORKDIR /usr/src/app
|
|
52
|
+
|
|
53
|
+
# Copy dependency manifests. `bunfig.toml` rides along so every install below
|
|
54
|
+
# passes its release-age gate and security scanner, as a local install does.
|
|
55
|
+
COPY package.json bun.lock bunfig.toml ./
|
|
56
|
+
|
|
57
|
+
# The scanner bunfig.toml names is a devDependency, and Bun installs a missing
|
|
58
|
+
# scanner through the same production-filtered install, which omits it and
|
|
59
|
+
# aborts. Seed it from the build stage's full install instead. Remove this line,
|
|
60
|
+
# and the `rm` at the end of this stage, if bunfig.toml stops naming a scanner.
|
|
61
|
+
COPY --from=build /usr/src/app/node_modules/@socketsecurity/bun-security-scanner ./node_modules/@socketsecurity/bun-security-scanner
|
|
62
|
+
|
|
63
|
+
# Docker names the target architecture `amd64`/`arm64`; Bun's `--cpu` takes
|
|
64
|
+
# `x64`/`arm64`. Mapped once here, read by both installs below. `oven/bun`
|
|
65
|
+
# publishes only these two architectures, so any other target fails here.
|
|
66
|
+
ARG TARGETOS
|
|
67
|
+
ARG TARGETARCH
|
|
68
|
+
RUN case "$TARGETARCH" in \
|
|
69
|
+
amd64) echo x64 ;; \
|
|
70
|
+
arm64) echo arm64 ;; \
|
|
71
|
+
*) echo "Unsupported TARGETARCH '$TARGETARCH': expected amd64 or arm64" >&2; exit 1 ;; \
|
|
72
|
+
esac > .bun-cpu
|
|
73
|
+
|
|
74
|
+
# Install only production dependencies, ignoring any lifecycle scripts (like 'prepare')
|
|
75
|
+
# that are not needed in the final production image.
|
|
76
|
+
# `--omit=peer` drops the framework's optional peer tiers (test runner, service
|
|
77
|
+
# SDKs, parsers) that Bun would otherwise auto-install. Anything this server
|
|
78
|
+
# actually imports belongs in its own `dependencies`, so nothing needed at
|
|
79
|
+
# runtime is lost.
|
|
80
|
+
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
81
|
+
bun install --production --omit=peer --frozen-lockfile --ignore-scripts \
|
|
82
|
+
--os="$TARGETOS" --cpu="$(cat .bun-cpu)"
|
|
83
|
+
|
|
84
|
+
# Conditionally install OpenTelemetry optional peer dependencies (Tier 3).
|
|
85
|
+
# Installed by default. Omit them for a leaner image at build time
|
|
86
|
+
# with: docker build --build-arg OTEL_ENABLED=false
|
|
87
|
+
# The script reads the list and each range from the installed framework's
|
|
88
|
+
# `peerDependencies` and passes the target flags on to its `bun install`.
|
|
89
|
+
COPY scripts/install-otel.ts ./scripts/
|
|
90
|
+
ARG OTEL_ENABLED=true
|
|
91
|
+
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
92
|
+
if [ "$OTEL_ENABLED" = "true" ]; then \
|
|
93
|
+
bun scripts/install-otel.ts --os="$TARGETOS" --cpu="$(cat .bun-cpu)"; \
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# `--os`/`--cpu` have no libc counterpart, so a native dependency published in
|
|
97
|
+
# glibc and musl variants (DuckDB's bindings, for one) installs both. The
|
|
98
|
+
# runtime image is Debian (glibc) and never loads the musl copy; the script
|
|
99
|
+
# deletes every package whose own `libc` admits only musl. It follows every
|
|
100
|
+
# install, since a later `bun install` restores what it removes. A server on a
|
|
101
|
+
# musl (Alpine) runtime image drops these two lines.
|
|
102
|
+
COPY scripts/prune-musl-packages.ts ./scripts/
|
|
103
|
+
RUN bun scripts/prune-musl-packages.ts
|
|
104
|
+
|
|
105
|
+
# The seeded scanner served only the installs above; keep it out of the image.
|
|
106
|
+
RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
|
|
107
|
+
|
|
108
|
+
|
|
30
109
|
# ==============================================================================
|
|
31
110
|
# Production Stage
|
|
32
111
|
#
|
|
33
112
|
# This stage creates a minimal, optimized, and secure image for running the
|
|
34
113
|
# application. It uses a slim base image and only includes production
|
|
35
|
-
# dependencies and build artifacts.
|
|
114
|
+
# dependencies and build artifacts. Its only Bun invocations are HEALTHCHECK
|
|
115
|
+
# and CMD, which run on the real target at container start.
|
|
36
116
|
# ==============================================================================
|
|
37
|
-
FROM oven/bun:1.4.
|
|
117
|
+
FROM oven/bun:1.4.2-slim AS production
|
|
38
118
|
|
|
39
119
|
WORKDIR /usr/src/app
|
|
40
120
|
|
|
41
|
-
# Set the environment to production for performance
|
|
42
|
-
# production dependencies are installed.
|
|
121
|
+
# Set the environment to production for performance.
|
|
43
122
|
ENV NODE_ENV=production
|
|
44
123
|
|
|
45
124
|
# OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
|
|
@@ -50,44 +129,37 @@ LABEL org.opencontainers.image.licenses="Apache-2.0"
|
|
|
50
129
|
LABEL org.opencontainers.image.version="${APP_VERSION}"
|
|
51
130
|
LABEL org.opencontainers.image.source="https://github.com/cyanheads/epa-mcp-server"
|
|
52
131
|
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
# that are not needed in the final production image.
|
|
58
|
-
# `--omit=peer` drops the framework's optional peer tiers that Bun would
|
|
59
|
-
# otherwise auto-install. The OTEL step below carries the same flag so that
|
|
60
|
-
# install does not re-resolve the optional peer graph.
|
|
61
|
-
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
62
|
-
bun install --production --omit=peer --frozen-lockfile --ignore-scripts
|
|
63
|
-
|
|
64
|
-
# Conditionally install OpenTelemetry optional peer dependencies (Tier 3).
|
|
65
|
-
# These are not bundled by default to keep the base image lean. Enable at build time
|
|
66
|
-
# with: docker build --build-arg OTEL_ENABLED=true
|
|
67
|
-
ARG OTEL_ENABLED=true
|
|
68
|
-
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
69
|
-
if [ "$OTEL_ENABLED" = "true" ]; then \
|
|
70
|
-
bun add --omit=dev --omit=peer --ignore-scripts @hono/otel \
|
|
71
|
-
@opentelemetry/instrumentation-http \
|
|
72
|
-
@opentelemetry/exporter-metrics-otlp-http \
|
|
73
|
-
@opentelemetry/exporter-trace-otlp-http \
|
|
74
|
-
@opentelemetry/instrumentation-pino \
|
|
75
|
-
@opentelemetry/resources \
|
|
76
|
-
@opentelemetry/sdk-metrics \
|
|
77
|
-
@opentelemetry/sdk-node \
|
|
78
|
-
@opentelemetry/sdk-trace-node \
|
|
79
|
-
@opentelemetry/semantic-conventions; \
|
|
80
|
-
fi
|
|
132
|
+
# The manifest comes from the build context: the deps stage's copy was rewritten
|
|
133
|
+
# by the OTel install, and the runtime reads only its name, version, and type.
|
|
134
|
+
COPY package.json ./
|
|
135
|
+
COPY --from=deps /usr/src/app/node_modules ./node_modules
|
|
81
136
|
|
|
82
137
|
# Copy the compiled application code from the build stage
|
|
83
138
|
COPY --from=build /usr/src/app/dist ./dist
|
|
84
139
|
|
|
140
|
+
# Mirror CLI (MirrorService adopters only — Tier 3, opt-in):
|
|
141
|
+
# Copy your mirror lifecycle scripts and emit a runtime tsconfig so Bun resolves
|
|
142
|
+
# the @/ path alias against ./dist/ rather than ./src/.
|
|
143
|
+
# See the api-mirror skill for the full recipe.
|
|
144
|
+
#
|
|
145
|
+
# COPY --from=build /usr/src/app/scripts/<your>-mirror-init.ts \
|
|
146
|
+
# /usr/src/app/scripts/<your>-mirror-refresh.ts \
|
|
147
|
+
# /usr/src/app/scripts/<your>-mirror-verify.ts \
|
|
148
|
+
# /usr/src/app/scripts/_mirror-context.ts \
|
|
149
|
+
# ./scripts/
|
|
150
|
+
# RUN echo '{"compilerOptions":{"baseUrl":".","paths":{"@/*":["./dist/*"]}}}' > tsconfig.json
|
|
151
|
+
|
|
85
152
|
# The 'oven/bun' image already provides a non-root user named 'bun'.
|
|
86
153
|
# We will use this existing user for enhanced security.
|
|
87
154
|
|
|
88
155
|
# Create and set permissions for the log directory, assigning ownership to the 'bun' user.
|
|
89
156
|
RUN mkdir -p /var/log/epa-mcp-server && chown -R bun:bun /var/log/epa-mcp-server
|
|
90
157
|
|
|
158
|
+
# Writable data dirs for on-disk SQLite stores (catalog index / observations
|
|
159
|
+
# mirror), owned by the runtime user. Mount a volume over either in production.
|
|
160
|
+
RUN mkdir -p /usr/src/app/.cache /usr/src/app/.mirror \
|
|
161
|
+
&& chown -R bun:bun /usr/src/app/.cache /usr/src/app/.mirror
|
|
162
|
+
|
|
91
163
|
# Switch to the non-root user
|
|
92
164
|
USER bun
|
|
93
165
|
|
|
@@ -103,7 +175,6 @@ ENV MCP_TRANSPORT_TYPE="http"
|
|
|
103
175
|
ENV MCP_SESSION_MODE="stateless"
|
|
104
176
|
ENV MCP_LOG_LEVEL="info"
|
|
105
177
|
ENV LOGS_DIR="/var/log/epa-mcp-server"
|
|
106
|
-
ENV MCP_FORCE_CONSOLE_LOGGING="true"
|
|
107
178
|
|
|
108
179
|
# Expose the port the server listens on
|
|
109
180
|
EXPOSE ${MCP_HTTP_PORT}
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
<div align="center">
|
|
9
9
|
|
|
10
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/epa-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/epa-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -19,11 +19,17 @@
|
|
|
19
19
|
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
|
+
<div align="center">
|
|
23
|
+
|
|
24
|
+
**Public Hosted Server:** [https://epa.caseyjhand.com/mcp](https://epa.caseyjhand.com/mcp)
|
|
25
|
+
|
|
26
|
+
</div>
|
|
27
|
+
|
|
22
28
|
---
|
|
23
29
|
|
|
24
30
|
## Overview
|
|
25
31
|
|
|
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
|
|
32
|
+
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, a local Streamable HTTP server, or the public hosted endpoint above.
|
|
27
33
|
|
|
28
34
|
### Tools
|
|
29
35
|
|
|
@@ -32,7 +38,7 @@ EPA environmental data across five federal programs — facility compliance (ECH
|
|
|
32
38
|
| `epa_search_facilities` | Search EPA-regulated facilities by location, industry program, or compliance status across CAA, CWA, RCRA, TRI, and SDWA |
|
|
33
39
|
| `epa_get_facility` | Full compliance profile for one facility by EPA Registry ID — inspections, enforcement actions, and penalties |
|
|
34
40
|
| `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
|
|
41
|
+
| `epa_get_air_quality` | Current AQI observations or the issued AQI forecast from AirNow |
|
|
36
42
|
| `epa_get_tri_releases` | Per-chemical Toxic Release Inventory data for a single facility |
|
|
37
43
|
| `epa_search_tri_releases` | Toxic Release Inventory records across facilities in a state or county |
|
|
38
44
|
| `epa_search_superfund` | Search Superfund (CERCLA/SEMS) sites by location or NPL listing status |
|
|
@@ -52,96 +58,90 @@ All resource data is also reachable via tools — use `epa_get_facility` and `ep
|
|
|
52
58
|
|
|
53
59
|
### `epa_search_facilities` <sub>tool</sub>
|
|
54
60
|
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
- Up to 100 results per call (default 50)
|
|
61
|
+
- Provide ZIP code, state, city (preferably with state), or latitude + longitude + radius_miles (max 100 miles); up to 100 results (default 50)
|
|
62
|
+
- Returns `registryId` for `epa_get_facility`, plus `fipsCode` when available for Census queries
|
|
63
|
+
- `programs` filters CAA, CWA, RCRA, TRI, or SDWA; `has_violation` filters significant violations
|
|
59
64
|
|
|
60
65
|
---
|
|
61
66
|
|
|
62
67
|
### `epa_get_facility` <sub>tool</sub>
|
|
63
68
|
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
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
|
|
69
|
+
- `registry_id` comes from `epa_search_facilities`
|
|
70
|
+
- Returns compliance, inspections, enforcement actions, penalties, and TRI totals; available program data survives partial upstream failures
|
|
71
|
+
- `airCompliance` / `waterCompliance` appear only for registered programs; `facility_not_found` identifies a missing record
|
|
69
72
|
|
|
70
73
|
---
|
|
71
74
|
|
|
72
75
|
### `epa_search_violations` <sub>tool</sub>
|
|
73
76
|
|
|
74
|
-
-
|
|
75
|
-
- `program`
|
|
76
|
-
-
|
|
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)
|
|
77
|
+
- Requires `state` or `zip_code`; up to 100 cases (default 50)
|
|
78
|
+
- `program` filters CAA, CWA, RCRA, SDWA, CERCLA, FIFRA, or TSCA; `case_type` is civil, criminal, or all (default); `date_filed_start` / `date_filed_end` filter ISO dates
|
|
79
|
+
- `facilityName` and `registryId` are unavailable from this endpoint; discover facilities with `epa_search_facilities`
|
|
79
80
|
|
|
80
81
|
---
|
|
81
82
|
|
|
82
83
|
### `epa_get_air_quality` <sub>tool</sub>
|
|
83
84
|
|
|
84
|
-
- Provide `zip_code` or both `latitude` and `longitude`
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
- `
|
|
88
|
-
-
|
|
89
|
-
- Only registered when `AIRNOW_API_KEY` is set
|
|
85
|
+
- Provide `zip_code` or both `latitude` and `longitude`; `mode` is current (default) or forecast
|
|
86
|
+
- Current mode returns each pollutant's latest hourly NowCast AQI from the closest monitor within 50 miles, with `siteName`, `siteID`, and `reportingAgency`
|
|
87
|
+
- Forecast mode returns every day the reporting area's agency issued, with `dateValid`, `actionDay`, and the agency's `discussion`; optional `forecast_date` (YYYY-MM-DD) keeps one day and is rejected in current mode. Category-only forecasts carry no `aqi`
|
|
88
|
+
- Readings carry `categoryName` and `categoryNumber` (1 Good–6 Hazardous); `attribution` credits the agencies and the U.S. EPA AirNow program and marks the data preliminary, unsuitable for regulatory, trend, or enforcement decisions
|
|
89
|
+
- Registered only when `AIRNOW_API_KEY` is set; responses cache for about one hour
|
|
90
90
|
|
|
91
91
|
---
|
|
92
92
|
|
|
93
93
|
### `epa_get_tri_releases` <sub>tool</sub>
|
|
94
94
|
|
|
95
|
-
- `facility_id` is the TRI `facilityId` from `epa_search_tri_releases`; optional `year` (1987–2030,
|
|
96
|
-
-
|
|
97
|
-
- TRI
|
|
95
|
+
- `facility_id` is the TRI `facilityId` from `epa_search_tri_releases`; optional `year` (1987–2030, all available by default) and partial `chemical_name`
|
|
96
|
+
- Returns per-chemical air, water, land, and underground-injection releases, plus a separate one-time/non-routine total
|
|
97
|
+
- Quantities are in pounds. TRI has facilities report dioxin and dioxin-like compounds in grams; those records are converted to pounds and carry `reportedUnit: "grams"`
|
|
98
98
|
|
|
99
99
|
---
|
|
100
100
|
|
|
101
101
|
### `epa_search_tri_releases` <sub>tool</sub>
|
|
102
102
|
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
103
|
+
- Requires 2-letter `state`; optional `county` (bare name, case-insensitive partial match, so `Lake` also matches LAKE OF THE WOODS — each record's `countyName` says which; a trailing County, Parish, or Borough is dropped), partial `chemical_name`, and `year`; up to 200 records (default 50)
|
|
104
|
+
- One upstream request however sparse the filters or large the state — EPA joins facilities to their reporting forms server-side
|
|
105
|
+
- Returns each record's one-time/non-routine total; `include_release_breakdown: true` adds air, water, land, and underground-injection releases at the cost of one more upstream request (up to ~20 seconds when EPA has not cached it)
|
|
106
|
+
- Returns `facilityId` for `epa_get_tri_releases`; enrichment marks results truncated when the limit is reached
|
|
107
|
+
- Dioxin and dioxin-like compounds, reported to TRI in grams, are converted to pounds and carry `reportedUnit: "grams"`
|
|
106
108
|
|
|
107
109
|
---
|
|
108
110
|
|
|
109
111
|
### `epa_search_superfund` <sub>tool</sub>
|
|
110
112
|
|
|
111
|
-
-
|
|
112
|
-
- `npl_status`
|
|
113
|
-
- Up to 200 sites per call (default 50)
|
|
113
|
+
- Provide `state`/`city`/`zip_code`, or `latitude` + `longitude` + `radius_miles` (0.1–500); up to 200 sites (default 50)
|
|
114
|
+
- Returns site identifiers, location, NPL, and cleanup status; `npl_status` filters listed, not-listed, proposed, or all (default)
|
|
114
115
|
|
|
115
116
|
---
|
|
116
117
|
|
|
117
118
|
### `epa_search_water_systems` <sub>tool</sub>
|
|
118
119
|
|
|
119
|
-
-
|
|
120
|
-
- `has_violation`
|
|
121
|
-
-
|
|
120
|
+
- Requires `state` or `zip_code`; up to 200 systems (default 50). `zip_code` matches each system's address of record (often the owner's or operator's office), not its service area
|
|
121
|
+
- `has_violation` keeps systems with an open SDWIS violation; violations returned to compliance or closed out when the system was deactivated don't count. `pws_type` is community, non-transient, or transient, returned as CWS / NTNCWS / TNCWS in `type`
|
|
122
|
+
- With `state` alone, `has_violation` reads the state's first 1,000 open violation records; with `zip_code`, it reads up to 1,000 systems in the ZIP and checks them by PWSID. When either read fills, an enrichment `notice` says the list may miss violating systems
|
|
122
123
|
|
|
123
124
|
---
|
|
124
125
|
|
|
125
126
|
### `epa_get_ejscreen` <sub>tool</sub>
|
|
126
127
|
|
|
127
|
-
-
|
|
128
|
-
- Returns 13 environmental and 6 demographic indicators
|
|
129
|
-
-
|
|
130
|
-
- Data source: EJScreen v2.2 (2022) via the community-maintained EJAM API (Public Environmental Data Partners) — not a live EPA endpoint
|
|
128
|
+
- Requires `latitude` and `longitude`; `distance` defaults to 1, `unit` to miles (kilometers accepted); buffer capped at 15 miles
|
|
129
|
+
- Returns 13 environmental and 6 demographic indicators with national/state percentiles and demographic indices; outside coverage, `coverage.valid: false` carries a note
|
|
130
|
+
- EJScreen v2.2 (2022) comes from the community-maintained EJAM API (Public Environmental Data Partners)
|
|
131
131
|
|
|
132
132
|
---
|
|
133
133
|
|
|
134
134
|
### `epa://facility/{registry_id}` <sub>resource</sub>
|
|
135
135
|
|
|
136
|
-
-
|
|
137
|
-
-
|
|
136
|
+
- `registry_id` comes from `epa_search_facilities`
|
|
137
|
+
- Returns the `epa_get_facility` compliance profile; errors when no facility resolves
|
|
138
138
|
|
|
139
139
|
---
|
|
140
140
|
|
|
141
141
|
### `epa://superfund/{site_id}` <sub>resource</sub>
|
|
142
142
|
|
|
143
|
-
-
|
|
144
|
-
-
|
|
143
|
+
- `site_id` comes from `epa_search_superfund`
|
|
144
|
+
- Returns the Superfund site record; errors when no SEMS record resolves
|
|
145
145
|
|
|
146
146
|
## Features
|
|
147
147
|
|
|
@@ -152,6 +152,7 @@ EPA-specific:
|
|
|
152
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)
|
|
153
153
|
- Parallel ECHO DFR aggregation in `epa_get_facility` — 3–5 upstream calls resolved concurrently with `Promise.allSettled`
|
|
154
154
|
- AirNow response caching (~1 hour TTL) to stay within per-key rate limits
|
|
155
|
+
- TRI reporting lags about 18 months; the latest available reporting year is typically two years behind
|
|
155
156
|
|
|
156
157
|
Agent-friendly output:
|
|
157
158
|
|
|
@@ -161,6 +162,23 @@ Agent-friendly output:
|
|
|
161
162
|
|
|
162
163
|
## Getting started
|
|
163
164
|
|
|
165
|
+
### Public Hosted Instance
|
|
166
|
+
|
|
167
|
+
A public instance is available at `https://epa.caseyjhand.com/mcp` — no installation required. It runs without an AirNow key, so `epa_get_air_quality` is not available there; the other 8 tools are. Point any MCP client at it via Streamable HTTP:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"mcpServers": {
|
|
172
|
+
"epa-mcp-server": {
|
|
173
|
+
"type": "streamable-http",
|
|
174
|
+
"url": "https://epa.caseyjhand.com/mcp"
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Self-Hosted / Local
|
|
181
|
+
|
|
164
182
|
Add the following to your MCP client configuration file. An AirNow API key is optional — set `AIRNOW_API_KEY` to enable `epa_get_air_quality` (register free at [docs.airnowapi.org](https://docs.airnowapi.org/account/request/)); without it the server starts with the other 8 tools. ECHO, DMAP, and EJScreen tools work without authentication.
|
|
165
183
|
|
|
166
184
|
```json
|
|
@@ -227,7 +245,7 @@ MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 AIRNOW_API_KEY=... bun run start:http
|
|
|
227
245
|
|
|
228
246
|
### Prerequisites
|
|
229
247
|
|
|
230
|
-
- [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
|
|
248
|
+
- [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+); development and Docker use Bun v1.4.2.
|
|
231
249
|
- (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.
|
|
232
250
|
|
|
233
251
|
### Installation
|
|
@@ -274,9 +292,14 @@ All configuration is validated at startup via Zod schemas in `src/config/`. Key
|
|
|
274
292
|
| `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` |
|
|
275
293
|
| `MCP_AUTH_MODE` | Auth mode: `none`, `jwt`, or `oauth` | `none` |
|
|
276
294
|
| `MCP_LOG_LEVEL` | Log level (RFC 5424) | `info` |
|
|
295
|
+
| `LOG_TOOL_FAILURE_PAYLOADS` | Log failed calls' arguments and results with key-name redaction. Secrets inside free-form values remain visible. | `false` |
|
|
296
|
+
| `LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES` | UTF-8 byte cap per logged payload. | `16384` |
|
|
277
297
|
| `LOGS_DIR` | Directory for log files (Node.js only) | `<project-root>/logs` |
|
|
278
298
|
| `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1` | `in-memory` |
|
|
279
299
|
| `OTEL_ENABLED` | Enable [OpenTelemetry](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry) tracing and metrics | `false` |
|
|
300
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP base URL; traces use `/v1/traces`, metrics `/v1/metrics`. | — |
|
|
301
|
+
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` / `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | Signal-specific endpoint overrides, used as-is. | — |
|
|
302
|
+
| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Enables OTLP log export; the base endpoint alone does not enable it. | — |
|
|
280
303
|
|
|
281
304
|
See [`.env.example`](./.env.example) for the full list of optional overrides.
|
|
282
305
|
|