@cyanheads/epa-mcp-server 0.3.2 → 0.3.3
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 +35 -42
- package/changelog/0.3.x/0.3.3.md +34 -0
- 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 +2 -6
- 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 +1 -3
- package/dist/mcp-server/tools/definitions/get-facility.tool.js.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 +2 -4
- 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 +2 -6
- package/dist/mcp-server/tools/definitions/search-superfund.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.map +1 -1
- package/dist/mcp-server/tools/definitions/search-water-systems.tool.js +1 -3
- package/dist/mcp-server/tools/definitions/search-water-systems.tool.js.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 +10 -9
- package/server.json +10 -24
package/AGENTS.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** epa-mcp-server
|
|
4
|
-
**Version:** 0.3.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.3.3
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.13`
|
|
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.3.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.3.3
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.13`
|
|
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
|
|
|
@@ -52,96 +52,83 @@ All resource data is also reachable via tools — use `epa_get_facility` and `ep
|
|
|
52
52
|
|
|
53
53
|
### `epa_search_facilities` <sub>tool</sub>
|
|
54
54
|
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
- Up to 100 results per call (default 50)
|
|
55
|
+
- Provide ZIP code, state, city (preferably with state), or latitude + longitude + radius_miles (max 100 miles); up to 100 results (default 50)
|
|
56
|
+
- Returns `registryId` for `epa_get_facility`, plus `fipsCode` when available for Census queries
|
|
57
|
+
- `programs` filters CAA, CWA, RCRA, TRI, or SDWA; `has_violation` filters significant violations
|
|
59
58
|
|
|
60
59
|
---
|
|
61
60
|
|
|
62
61
|
### `epa_get_facility` <sub>tool</sub>
|
|
63
62
|
|
|
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
|
|
63
|
+
- `registry_id` comes from `epa_search_facilities`
|
|
64
|
+
- Returns compliance, inspections, enforcement actions, penalties, and TRI totals; available program data survives partial upstream failures
|
|
65
|
+
- `airCompliance` / `waterCompliance` appear only for registered programs; `facility_not_found` identifies a missing record
|
|
69
66
|
|
|
70
67
|
---
|
|
71
68
|
|
|
72
69
|
### `epa_search_violations` <sub>tool</sub>
|
|
73
70
|
|
|
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)
|
|
71
|
+
- Requires `state` or `zip_code`; up to 100 cases (default 50)
|
|
72
|
+
- `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
|
|
73
|
+
- `facilityName` and `registryId` are unavailable from this endpoint; discover facilities with `epa_search_facilities`
|
|
79
74
|
|
|
80
75
|
---
|
|
81
76
|
|
|
82
77
|
### `epa_get_air_quality` <sub>tool</sub>
|
|
83
78
|
|
|
84
|
-
- Provide `zip_code` or both `latitude` and `longitude`
|
|
85
|
-
-
|
|
86
|
-
-
|
|
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
|
|
79
|
+
- Provide `zip_code` or both `latitude` and `longitude`; `mode` is current (default) or forecast (requires ISO `forecast_date`); `distance_miles` defaults to 25, max 300
|
|
80
|
+
- Returns per-pollutant AQI, `categoryNumber` (1 Good–6 Hazardous), and `categoryName`; preliminary data is unsuitable for regulatory, trend, or enforcement decisions
|
|
81
|
+
- Registered only when `AIRNOW_API_KEY` is set; responses cache for about one hour
|
|
90
82
|
|
|
91
83
|
---
|
|
92
84
|
|
|
93
85
|
### `epa_get_tri_releases` <sub>tool</sub>
|
|
94
86
|
|
|
95
|
-
- `facility_id` is the TRI `facilityId` from `epa_search_tri_releases`; optional `year` (1987–2030,
|
|
96
|
-
-
|
|
97
|
-
- TRI data lags ~18 months; the most recent available year is typically 2 years prior to the current year
|
|
87
|
+
- `facility_id` is the TRI `facilityId` from `epa_search_tri_releases`; optional `year` (1987–2030, all available by default) and partial `chemical_name`
|
|
88
|
+
- Returns per-chemical air, water, land, and underground-injection releases, plus a separate one-time/non-routine total
|
|
98
89
|
|
|
99
90
|
---
|
|
100
91
|
|
|
101
92
|
### `epa_search_tri_releases` <sub>tool</sub>
|
|
102
93
|
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
- Complement to `epa_get_tri_releases` — use this for area discovery, then drill into a specific facility
|
|
94
|
+
- Requires 2-letter `state`; optional partial `county`, `chemical_name`, and `year`; up to 200 records (default 50)
|
|
95
|
+
- Returns `facilityId` for `epa_get_tri_releases`; enrichment marks results truncated when the limit is reached
|
|
106
96
|
|
|
107
97
|
---
|
|
108
98
|
|
|
109
99
|
### `epa_search_superfund` <sub>tool</sub>
|
|
110
100
|
|
|
111
|
-
-
|
|
112
|
-
- `npl_status`
|
|
113
|
-
- Up to 200 sites per call (default 50)
|
|
101
|
+
- Provide `state`/`city`/`zip_code`, or `latitude` + `longitude` + `radius_miles` (0.1–500); up to 200 sites (default 50)
|
|
102
|
+
- Returns site identifiers, location, NPL, and cleanup status; `npl_status` filters listed, not-listed, proposed, or all (default)
|
|
114
103
|
|
|
115
104
|
---
|
|
116
105
|
|
|
117
106
|
### `epa_search_water_systems` <sub>tool</sub>
|
|
118
107
|
|
|
119
|
-
-
|
|
120
|
-
- `has_violation`
|
|
121
|
-
- Up to 200 systems per call (default 50)
|
|
108
|
+
- Requires `state` or `zip_code`; up to 200 systems (default 50)
|
|
109
|
+
- `has_violation` filters active violations; `pws_type` is community, non-transient, or transient, returned as CWS / NTNCWS / TNCWS in `type`
|
|
122
110
|
|
|
123
111
|
---
|
|
124
112
|
|
|
125
113
|
### `epa_get_ejscreen` <sub>tool</sub>
|
|
126
114
|
|
|
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
|
|
115
|
+
- Requires `latitude` and `longitude`; `distance` defaults to 1, `unit` to miles (kilometers accepted); buffer capped at 15 miles
|
|
116
|
+
- Returns 13 environmental and 6 demographic indicators with national/state percentiles and demographic indices; outside coverage, `coverage.valid: false` carries a note
|
|
117
|
+
- EJScreen v2.2 (2022) comes from the community-maintained EJAM API (Public Environmental Data Partners)
|
|
131
118
|
|
|
132
119
|
---
|
|
133
120
|
|
|
134
121
|
### `epa://facility/{registry_id}` <sub>resource</sub>
|
|
135
122
|
|
|
136
|
-
-
|
|
137
|
-
-
|
|
123
|
+
- `registry_id` comes from `epa_search_facilities`
|
|
124
|
+
- Returns the `epa_get_facility` compliance profile; errors when no facility resolves
|
|
138
125
|
|
|
139
126
|
---
|
|
140
127
|
|
|
141
128
|
### `epa://superfund/{site_id}` <sub>resource</sub>
|
|
142
129
|
|
|
143
|
-
-
|
|
144
|
-
-
|
|
130
|
+
- `site_id` comes from `epa_search_superfund`
|
|
131
|
+
- Returns the Superfund site record; errors when no SEMS record resolves
|
|
145
132
|
|
|
146
133
|
## Features
|
|
147
134
|
|
|
@@ -152,6 +139,7 @@ EPA-specific:
|
|
|
152
139
|
- 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
140
|
- Parallel ECHO DFR aggregation in `epa_get_facility` — 3–5 upstream calls resolved concurrently with `Promise.allSettled`
|
|
154
141
|
- AirNow response caching (~1 hour TTL) to stay within per-key rate limits
|
|
142
|
+
- TRI reporting lags about 18 months; the latest available reporting year is typically two years behind
|
|
155
143
|
|
|
156
144
|
Agent-friendly output:
|
|
157
145
|
|
|
@@ -227,7 +215,7 @@ MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 AIRNOW_API_KEY=... bun run start:http
|
|
|
227
215
|
|
|
228
216
|
### Prerequisites
|
|
229
217
|
|
|
230
|
-
- [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
|
|
218
|
+
- [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+); development and Docker use Bun v1.4.2.
|
|
231
219
|
- (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
220
|
|
|
233
221
|
### Installation
|
|
@@ -274,9 +262,14 @@ All configuration is validated at startup via Zod schemas in `src/config/`. Key
|
|
|
274
262
|
| `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
263
|
| `MCP_AUTH_MODE` | Auth mode: `none`, `jwt`, or `oauth` | `none` |
|
|
276
264
|
| `MCP_LOG_LEVEL` | Log level (RFC 5424) | `info` |
|
|
265
|
+
| `LOG_TOOL_FAILURE_PAYLOADS` | Log failed calls' arguments and results with key-name redaction. Secrets inside free-form values remain visible. | `false` |
|
|
266
|
+
| `LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES` | UTF-8 byte cap per logged payload. | `16384` |
|
|
277
267
|
| `LOGS_DIR` | Directory for log files (Node.js only) | `<project-root>/logs` |
|
|
278
268
|
| `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1` | `in-memory` |
|
|
279
269
|
| `OTEL_ENABLED` | Enable [OpenTelemetry](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry) tracing and metrics | `false` |
|
|
270
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP base URL; traces use `/v1/traces`, metrics `/v1/metrics`. | — |
|
|
271
|
+
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` / `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | Signal-specific endpoint overrides, used as-is. | — |
|
|
272
|
+
| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Enables OTLP log export; the base endpoint alone does not enable it. | — |
|
|
280
273
|
|
|
281
274
|
See [`.env.example`](./.env.example) for the full list of optional overrides.
|
|
282
275
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "ECHO debug logs use endpoint labels instead of request URLs, with framework recovery and error privacy updates."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 0.3.3 — 2026-10-07
|
|
8
|
+
|
|
9
|
+
## Changed
|
|
10
|
+
|
|
11
|
+
- **Domain recovery** uses the framework's declared-hint fill from 0.13.10; redundant forwards are removed and contract tests preserve hints on both MCP result surfaces.
|
|
12
|
+
- **Framework input and errors** adopt 0.13.9's integer-to-string repair and field-path guidance, 0.13.10's error request IDs, 0.13.12's generated IDs and URL-origin redaction, and 0.13.13's removal of framework-added `rootCause` from error envelopes.
|
|
13
|
+
- **Docker builds** use Bun 1.4.2 and a native build-platform dependency stage, cross-installing for amd64/arm64 with framework-range OTel peers and pruning musl-only bindings.
|
|
14
|
+
- **Framework tooling and guides** are synchronized; configuration documents OTLP endpoints and opt-in failed-call payload logging, packaging excludes `.mcpb`, cache, and logs, and CodeQL uploads per-language categories.
|
|
15
|
+
- **ECHO User-Agent** reports the current server version.
|
|
16
|
+
|
|
17
|
+
## Fixed
|
|
18
|
+
|
|
19
|
+
- **MCP Registry npm entries** launch with `npx`, drop npm-script arguments, and fix `MCP_TRANSPORT_TYPE=http` for the HTTP entry.
|
|
20
|
+
|
|
21
|
+
## Security
|
|
22
|
+
|
|
23
|
+
- **ECHO debug logs** identify facility and enforcement requests by fixed endpoint labels and query IDs, omitting full configured URL paths and query strings ([#26](https://github.com/cyanheads/epa-mcp-server/issues/26)).
|
|
24
|
+
|
|
25
|
+
## Dependencies
|
|
26
|
+
|
|
27
|
+
- `@cyanheads/mcp-ts-core` ^0.13.6 → ^0.13.13.
|
|
28
|
+
- Dev: `@biomejs/biome` ^2.5.14 → ^2.5.15.
|
|
29
|
+
- Dev: `@socketsecurity/bun-security-scanner` ^1.1.2 → ^1.1.3.
|
|
30
|
+
- Dev: `@types/node` ^26.6.1 → ^26.6.4.
|
|
31
|
+
- Dev: `ignore` ^7.0.9 → ^7.0.12.
|
|
32
|
+
- Dev: `tsc-alias` ^1.9.5 → ^1.9.7.
|
|
33
|
+
- Dev: `vitest` ^5.0.1 → ^5.0.3.
|
|
34
|
+
- **Bun** 1.4.0 → 1.4.2 in `packageManager` and Docker base images.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-air-quality.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/get-air-quality.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAIjE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"get-air-quality.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/get-air-quality.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAIjE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAmO5B,CAAC"}
|
|
@@ -110,17 +110,13 @@ export const getAirQualityTool = tool('epa_get_air_quality', {
|
|
|
110
110
|
? { kind: 'latlng', latitude: input.latitude, longitude: input.longitude }
|
|
111
111
|
: undefined;
|
|
112
112
|
if (!loc) {
|
|
113
|
-
throw ctx.fail('no_location', 'Provide either zip_code or both latitude and longitude.'
|
|
114
|
-
...ctx.recoveryFor('no_location'),
|
|
115
|
-
});
|
|
113
|
+
throw ctx.fail('no_location', 'Provide either zip_code or both latitude and longitude.');
|
|
116
114
|
}
|
|
117
115
|
// forecast mode requires a date — validate up front so the error path is unchanged.
|
|
118
116
|
let forecastDate = '';
|
|
119
117
|
if (input.mode === 'forecast') {
|
|
120
118
|
if (input.forecast_date === undefined || input.forecast_date.trim() === '') {
|
|
121
|
-
throw ctx.fail('forecast_date_required', 'forecast_date (YYYY-MM-DD) is required when mode is "forecast".'
|
|
122
|
-
...ctx.recoveryFor('forecast_date_required'),
|
|
123
|
-
});
|
|
119
|
+
throw ctx.fail('forecast_date_required', 'forecast_date (YYYY-MM-DD) is required when mode is "forecast".');
|
|
124
120
|
}
|
|
125
121
|
forecastDate = input.forecast_date;
|
|
126
122
|
}
|