@cyanheads/earthquake-mcp-server 0.1.16 → 0.3.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.
Files changed (50) hide show
  1. package/CLAUDE.md +31 -23
  2. package/Dockerfile +13 -7
  3. package/README.md +17 -8
  4. package/dist/index.js +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/mcp-server/resources/definitions/earthquake-event.resource.d.ts +5 -3
  7. package/dist/mcp-server/resources/definitions/earthquake-event.resource.d.ts.map +1 -1
  8. package/dist/mcp-server/resources/definitions/earthquake-feed.resource.d.ts.map +1 -1
  9. package/dist/mcp-server/resources/definitions/earthquake-feed.resource.js +4 -1
  10. package/dist/mcp-server/resources/definitions/earthquake-feed.resource.js.map +1 -1
  11. package/dist/mcp-server/tools/definitions/earthquake-count.tool.d.ts +34 -2
  12. package/dist/mcp-server/tools/definitions/earthquake-count.tool.d.ts.map +1 -1
  13. package/dist/mcp-server/tools/definitions/earthquake-count.tool.js +129 -5
  14. package/dist/mcp-server/tools/definitions/earthquake-count.tool.js.map +1 -1
  15. package/dist/mcp-server/tools/definitions/earthquake-get-event.tool.d.ts +5 -3
  16. package/dist/mcp-server/tools/definitions/earthquake-get-event.tool.d.ts.map +1 -1
  17. package/dist/mcp-server/tools/definitions/earthquake-get-event.tool.js +1 -1
  18. package/dist/mcp-server/tools/definitions/earthquake-get-event.tool.js.map +1 -1
  19. package/dist/mcp-server/tools/definitions/earthquake-get-feed.tool.d.ts +18 -4
  20. package/dist/mcp-server/tools/definitions/earthquake-get-feed.tool.d.ts.map +1 -1
  21. package/dist/mcp-server/tools/definitions/earthquake-get-feed.tool.js +97 -12
  22. package/dist/mcp-server/tools/definitions/earthquake-get-feed.tool.js.map +1 -1
  23. package/dist/mcp-server/tools/definitions/earthquake-search.tool.d.ts +22 -9
  24. package/dist/mcp-server/tools/definitions/earthquake-search.tool.d.ts.map +1 -1
  25. package/dist/mcp-server/tools/definitions/earthquake-search.tool.js +106 -31
  26. package/dist/mcp-server/tools/definitions/earthquake-search.tool.js.map +1 -1
  27. package/dist/mcp-server/tools/fdsn-error.d.ts +27 -0
  28. package/dist/mcp-server/tools/fdsn-error.d.ts.map +1 -0
  29. package/dist/mcp-server/tools/fdsn-error.js +71 -0
  30. package/dist/mcp-server/tools/fdsn-error.js.map +1 -0
  31. package/dist/mcp-server/tools/query-params.d.ts +7 -1
  32. package/dist/mcp-server/tools/query-params.d.ts.map +1 -1
  33. package/dist/mcp-server/tools/query-params.js +13 -1
  34. package/dist/mcp-server/tools/query-params.js.map +1 -1
  35. package/dist/mcp-server/tools/schemas.d.ts +5 -3
  36. package/dist/mcp-server/tools/schemas.d.ts.map +1 -1
  37. package/dist/mcp-server/tools/schemas.js +44 -12
  38. package/dist/mcp-server/tools/schemas.js.map +1 -1
  39. package/dist/services/emsc/emsc-service.d.ts +4 -2
  40. package/dist/services/emsc/emsc-service.d.ts.map +1 -1
  41. package/dist/services/emsc/emsc-service.js +29 -12
  42. package/dist/services/emsc/emsc-service.js.map +1 -1
  43. package/dist/services/usgs/types.d.ts +16 -3
  44. package/dist/services/usgs/types.d.ts.map +1 -1
  45. package/dist/services/usgs/usgs-service.d.ts +4 -2
  46. package/dist/services/usgs/usgs-service.d.ts.map +1 -1
  47. package/dist/services/usgs/usgs-service.js +22 -31
  48. package/dist/services/usgs/usgs-service.js.map +1 -1
  49. package/package.json +10 -9
  50. package/server.json +3 -3
package/CLAUDE.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  **Server:** earthquake-mcp-server
4
4
  **Package:** `@cyanheads/earthquake-mcp-server`
5
- **Version:** 0.1.16
6
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.10.10`
5
+ **Version:** 0.3.0
6
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.10.17`
7
7
  **Engines:** Bun ≥1.3.0, Node ≥24.0.0
8
8
  **MCP SDK:** `@modelcontextprotocol/sdk` ^1.29.0
9
9
  **Zod:** ^4.4.3
@@ -136,6 +136,8 @@ export function getServerConfig() {
136
136
 
137
137
  `parseEnvConfig` maps Zod schema paths → env var names so errors name the variable (`MY_API_KEY`) not the path (`apiKey`). Throws `ConfigurationError`, which the framework prints as a clean startup banner.
138
138
 
139
+ For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag can't be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else, so `=false` actually disables.
140
+
139
141
  ---
140
142
 
141
143
  ## Context
@@ -145,16 +147,18 @@ Handlers receive a unified `ctx` object. Key properties:
145
147
  | Property | Description |
146
148
  |:---------|:------------|
147
149
  | `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. |
148
- | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
149
- | `ctx.elicit` | Ask user for structured input. **Check for presence first:** `if (ctx.elicit) { ... }` |
150
+ | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
151
+ | `ctx.elicit` | Ask user for structured input — form call `(message, schema)` or `.url(message, url)` for an external link. **Check for presence first:** `if (ctx.elicit) { ... }` |
150
152
  | `ctx.sample` | Request LLM completion from the client. **Check for presence first:** `if (ctx.sample) { ... }` |
153
+ | `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). |
154
+ | `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`. |
151
155
  | `ctx.signal` | `AbortSignal` for cancellation. |
152
156
  | `ctx.progress` | Task progress (present when `task: true`) — `.setTotal(n)`, `.increment()`, `.update(message)`. |
153
157
  | `ctx.fail` | Throw a typed error checked against the `errors[]` contract — `ctx.fail('reason', message, { recovery? })`. |
154
158
  | `ctx.notifyPromptListChanged` | Notify clients that the prompt list changed (call after dynamic prompt registration). |
155
159
  | `ctx.notifyToolListChanged` | Notify clients that the tool list changed (call after dynamic tool registration). |
156
160
  | `ctx.requestId` | Unique request ID. |
157
- | `ctx.tenantId` | Tenant ID from JWT or `'default'` for stdio. |
161
+ | `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
158
162
 
159
163
  ---
160
164
 
@@ -162,7 +166,7 @@ Handlers receive a unified `ctx` object. Key properties:
162
166
 
163
167
  Handlers throw — the framework catches, classifies, and formats.
164
168
 
165
- **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required descriptive metadata for the agent's next move ( 5 words, lint-validated); for the wire `data.recovery.hint` (mirrored into `content[]` text), pass explicitly at the throw site when dynamic context matters: `ctx.fail('reason', msg, { recovery: { hint: '...' } })`. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring.
169
+ **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. Pass `ctx.recoveryFor('reason')` as the throw's data to put it on the wire (`data.recovery.hint`, mirrored into `content[]` text); override with an explicit `{ recovery: { hint: '...' } }` when dynamic runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring.
166
170
 
167
171
  ```ts
168
172
  import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
@@ -174,7 +178,7 @@ errors: [
174
178
  ],
175
179
  async handler(input, ctx) {
176
180
  const item = await db.find(input.id);
177
- if (!item) throw ctx.fail('no_match', `No item ${input.id}`);
181
+ if (!item) throw ctx.fail('no_match', `No item ${input.id}`, ctx.recoveryFor('no_match'));
178
182
  return item;
179
183
  }
180
184
  ```
@@ -237,7 +241,7 @@ src/
237
241
 
238
242
  ## Skills
239
243
 
240
- Skills are modular instructions in `skills/` at the project root. Read them directly when a task matches — e.g., `skills/add-tool/SKILL.md` when adding a tool.
244
+ Skills are modular instructions in `skills/` at the project root. Read them directly when a task matches — e.g., `skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry.
241
245
 
242
246
  **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
243
247
 
@@ -254,30 +258,29 @@ Available skills:
254
258
  | `add-service` | Scaffold a new service integration |
255
259
  | `add-test` | Scaffold test file for a tool, resource, or service |
256
260
  | `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
257
- | `tool-defs-analysis` | Read-only audit of definition language: voice, leaks, defaults, recovery hints, examples |
261
+ | `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
258
262
  | `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
259
263
  | `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
260
- | `devcheck` | Lint, format, typecheck, audit |
261
264
  | `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
262
- | `maintenance` | Investigate changelogs, adopt upstream changes, and sync skills after `bun update --latest` |
263
- | `git-wrapup` | Version-bump, changelog, commit, and tag workflow |
264
- | `release-and-publish` | Ship a release: verification gate, push commits+tags, publish to npm / MCP Registry / GHCR / `.mcpb` bundle on GitHub Releases |
265
+ | `maintenance` | Investigate changelogs, adopt upstream changes, sync skills to agent dirs |
266
+ | `git-wrapup` | Land working-tree changes as a versioned commit + annotated tag — version bump, changelog, verify, tag. Local only. |
267
+ | `release-and-publish` | Push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
265
268
  | `api-auth` | Auth modes, scopes, JWT/OAuth |
266
269
  | `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
267
270
  | `api-config` | AppConfig, parseConfig, env vars |
268
271
  | `api-context` | Context interface, logger, state, progress |
269
272
  | `api-errors` | McpError, JsonRpcErrorCode, error patterns |
270
- | `api-linter` | Definition lint rules reference (`format-parity`, `schema-*`, `server-json-*`, …) |
273
+ | `api-linter` | Definition linter rule catalog — invoked by `bun run lint:mcp` and `devcheck` |
274
+ | `api-mirror` | MirrorService: persistent self-refreshing local mirror (embedded SQLite + FTS5) of a bulk upstream dataset — Tier 3 opt-in |
271
275
  | `api-services` | LLM, Speech, Graph services |
272
276
  | `api-telemetry` | OTel catalog: spans, metrics, completion logs, env config, cardinality rules |
273
277
  | `api-testing` | createMockContext, test patterns |
274
278
  | `api-utils` | Formatting, parsing, security, pagination, scheduling, telemetry helpers |
275
- | `api-mirror` | MirrorService — persistent local SQLite mirror of a bulk upstream dataset (Tier 3 opt-in) |
276
279
  | `api-workers` | Cloudflare Workers runtime |
277
- | `techniques` | Catalog of reusable response/data-shaping patterns — overflow handling, payload shaping, retrieval when a payload is too large or awkwardly shaped |
280
+ | `techniques` | Catalog of response/data-shaping techniques — overflow handling, payload shaping, retrieval patterns |
278
281
  | `orchestrations` | Chain task skills into a gated multi-phase pipeline — build-out, QA-fix, update-ship — when you can spawn sub-agents |
279
- | `report-issue-framework` | File bug/feature request against @cyanheads/mcp-ts-core |
280
- | `report-issue-local` | File bug/feature request against this server's repo |
282
+ | `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
283
+ | `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
281
284
 
282
285
  **Chaining skills into pipelines.** When the user wants a multi-phase effort — build this server out, QA-and-fix the surface, update-and-ship — *and you can spawn sub-agents*, `skills/orchestrations/SKILL.md` sequences the task skills above into a gated pipeline with verification at each step. Read it to drive the run. Optional: skip it if you can't orchestrate sub-agents, and ignore it entirely if you were *spawned* as one — you've already been scoped to a single phase.
283
286
 
@@ -292,11 +295,14 @@ When you complete a skill's checklist, check the boxes and add a completion time
292
295
  | `bun run build` | Compile TypeScript |
293
296
  | `bun run rebuild` | Clean + build |
294
297
  | `bun run clean` | Remove build artifacts |
295
- | `bun run devcheck` | Lint + format + typecheck + security + packaging alignment |
298
+ | `bun run devcheck` | Lint + format + typecheck + security + packaging alignment + changelog sync |
296
299
  | `bun run audit:refresh` | Delete `bun.lock`, reinstall, and re-run `bun audit`. Use when `devcheck` flags a transitive advisory — Bun's `update` is sticky on transitive resolutions, so the advisory may be a stale-lockfile false positive. If it survives the refresh, it's real. |
297
300
  | `bun run tree` | Generate directory structure doc |
298
- | `bun run format` | Auto-fix formatting |
299
- | `bun run test` | Run tests |
301
+ | `bun run format` | Auto-fix formatting (safe fixes only) |
302
+ | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
303
+ | `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
304
+ | `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
305
+ | `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
300
306
  | `bun run lint:mcp` | Validate MCP definitions against spec |
301
307
  | `bun run lint:packaging` | Validate env var alignment between `manifest.json` and `server.json` (skipped cleanly when `manifest.json` is absent) |
302
308
  | `bun run list-skills` | List skills in `skills/` with name + description |
@@ -342,14 +348,16 @@ Each per-version file opens with YAML frontmatter:
342
348
  ---
343
349
  summary: "One-line headline, ≤350 chars" # required — powers the rollup index
344
350
  breaking: false # optional — true flags breaking changes
345
- security: false # optional — true flags security fixes
351
+ security: false # optional — true ONLY for a source-code security fix, never a dependency CVE bump
346
352
  ---
347
353
 
348
354
  # 0.1.0 — YYYY-MM-DD
349
355
  ...
350
356
  ```
351
357
 
352
- `breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section. When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
358
+ `breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section — set it only for a security fix in this server's *own source code*, never for a routine dependency or transitive CVE bump (record those under `## Dependencies`). When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
359
+
360
+ `agent-notes` is an optional free-form field for maintenance agents processing the release downstream. Content here won't appear in the rendered CHANGELOG — it's consumed by agents running the `maintenance` skill. Use it for adoption instructions that don't fit the human-facing sections: new files to create, fields to populate, one-time migration steps. Omit entirely when there's nothing to say.
353
361
 
354
362
  **Section order** (Keep a Changelog): Added, Changed, Deprecated, Removed, Fixed, Security. Include only sections with entries — don't ship empty headers.
355
363
 
package/Dockerfile CHANGED
@@ -4,15 +4,19 @@
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
- FROM oven/bun:1.3 AS build
7
+ FROM oven/bun:1.3.14 AS build
8
8
 
9
9
  WORKDIR /usr/src/app
10
10
 
11
11
  # Copy dependency manifests for optimized layer caching
12
12
  COPY package.json bun.lock ./
13
13
 
14
- # Install all dependencies (including dev dependencies for building)
15
- RUN bun install --frozen-lockfile
14
+ # Install all dependencies (including dev dependencies for building).
15
+ # The BuildKit cache mount persists Bun's global package cache across builds.
16
+ # --ignore-scripts keeps a dependency's native postinstall (node-gyp under Bun)
17
+ # from aborting the build; tsc needs type declarations, not compiled bindings.
18
+ RUN --mount=type=cache,target=/root/.bun/install/cache \
19
+ bun install --frozen-lockfile --ignore-scripts
16
20
 
17
21
  # Copy the rest of the source code
18
22
  COPY . .
@@ -29,7 +33,7 @@ RUN bun scripts/build.ts
29
33
  # application. It uses a slim base image and only includes production
30
34
  # dependencies and build artifacts.
31
35
  # ==============================================================================
32
- FROM oven/bun:1.3-slim AS production
36
+ FROM oven/bun:1.3.14-slim AS production
33
37
 
34
38
  WORKDIR /usr/src/app
35
39
 
@@ -50,14 +54,16 @@ COPY package.json bun.lock ./
50
54
 
51
55
  # Install only production dependencies, ignoring any lifecycle scripts (like 'prepare')
52
56
  # that are not needed in the final production image.
53
- RUN bun install --production --frozen-lockfile --ignore-scripts
57
+ RUN --mount=type=cache,target=/root/.bun/install/cache \
58
+ bun install --production --frozen-lockfile --ignore-scripts
54
59
 
55
60
  # Conditionally install OpenTelemetry optional peer dependencies (Tier 3).
56
61
  # These are not bundled by default to keep the base image lean. Enable at build time
57
62
  # with: docker build --build-arg OTEL_ENABLED=true
58
63
  ARG OTEL_ENABLED=true
59
- RUN if [ "$OTEL_ENABLED" = "true" ]; then \
60
- bun add @hono/otel \
64
+ RUN --mount=type=cache,target=/root/.bun/install/cache \
65
+ if [ "$OTEL_ENABLED" = "true" ]; then \
66
+ bun add --omit=dev --ignore-scripts @hono/otel \
61
67
  @opentelemetry/instrumentation-http \
62
68
  @opentelemetry/exporter-metrics-otlp-http \
63
69
  @opentelemetry/exporter-trace-otlp-http \
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-0.1.16-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/earthquake-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.29.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/earthquake-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/earthquake-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^6.0.3-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-0.3.0-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/earthquake-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.29.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/earthquake-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/earthquake-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^6.0.3-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -46,6 +46,8 @@ Fetch a USGS pre-computed real-time earthquake feed by magnitude tier and time w
46
46
  - Five magnitude tiers: `all` (microseisms), `1.0`, `2.5`, `4.5`, and `significant` (USGS-curated by magnitude, felt reports, and PAGER impact)
47
47
  - Four time windows: `hour`, `day`, `week`, `month`
48
48
  - Returns event list with counts and the source feed URL
49
+ - Paged with an opaque `cursor`: `limit` bounds a page (default 100, max 1000), `totalCount` reports the whole feed, and `nextCursor` retrieves the rest — the broad tiers run past 10,000 events for `month`
50
+ - The cursor is opaque because these feeds have no upstream paging parameter and USGS regenerates them about once a minute; a numeric offset across two calls would skip or repeat events
49
51
  - Best for real-time "what's happening now" queries; use `earthquake_search` for historical or filtered queries
50
52
 
51
53
  ---
@@ -54,13 +56,15 @@ Fetch a USGS pre-computed real-time earthquake feed by magnitude tier and time w
54
56
 
55
57
  Search earthquakes by time range, magnitude, depth, location radius, PAGER alert level, or felt reports.
56
58
 
57
- - Dual-source: USGS (global, richer metadata) or EMSC (European-Mediterranean, independent catalog for cross-verification)
59
+ - Dual-source: USGS (global, richer metadata) or EMSC (an independent global catalog from the European-Mediterranean Seismological Centre, for cross-verification anywhere)
58
60
  - Full FDSN ComCat query API parameters: time range, magnitude, depth, location radius
59
61
  - USGS-specific filters: PAGER alert level (`green`/`yellow`/`orange`/`red`), DYFI felt reports count, significance score
60
62
  - Location-based queries: provide `latitude`, `longitude`, and `radius_km` together
61
63
  - Sort by time (newest first) or magnitude (largest first), ascending or descending
62
- - Results capped at 20,000 events per query; use `earthquake_count` first to gauge result size
63
- - USGS-specific filters are silently ignored when `source=emsc`
64
+ - One call returns at most 20,000 events; page beyond that with `offset`, forwarded straight to the upstream FDSN `offset` parameter on both sources
65
+ - `offset` counts from 1, matching both upstream APIs — a capped result carries `totalCount` and the `nextOffset` to pass on the following call
66
+ - Use `earthquake_count` first to gauge result size
67
+ - USGS-specific filters are not supported by EMSC — when `source=emsc` they are dropped and named in `ignoredFilters`, so an unconstrained result set is never mistaken for a filtered one
64
68
 
65
69
  ---
66
70
 
@@ -70,8 +74,10 @@ Count earthquakes matching filters without fetching full records.
70
74
 
71
75
  - Lightweight alternative to `earthquake_search` for statistical queries ("how many M5+ events in 2025?")
72
76
  - Same filter surface as `earthquake_search`: time, magnitude, depth, location radius, PAGER, DYFI, significance
73
- - Returns `exceeds_limit` flag when count exceeds 20,000 — signals that a full search would be truncated
77
+ - Returns `exceeds_limit` flag when count exceeds 20,000 — signals a full search needs paging
78
+ - Echoes the effective query back as `queryEcho`, including the resolved time window — omitting `start_time` counts only the last 30 days
74
79
  - USGS returns the `max_allowed` cap (20,000); EMSC count endpoint does not expose this field (`max_allowed` will be null)
80
+ - USGS-specific filters are dropped and named in `ignoredFilters` when `source=emsc`, the same as on `earthquake_search`
75
81
 
76
82
  ---
77
83
 
@@ -87,7 +93,7 @@ Fetch complete detail for a specific earthquake by USGS event ID.
87
93
 
88
94
  | Type | URI pattern | Description |
89
95
  |:---|:---|:---|
90
- | Resource | `earthquake://feed/{magnitude_tier}/{time_window}` | USGS real-time earthquake feed as injectable context |
96
+ | Resource | `earthquake://feed/{magnitude_tier}/{time_window}` | USGS real-time earthquake feed as injectable context — returns the whole feed, so use the `earthquake_get_feed` tool for the broad tiers |
91
97
  | Resource | `earthquake://event/{event_id}` | Full USGS earthquake event detail by ID as injectable context |
92
98
 
93
99
  ## Features
@@ -103,7 +109,7 @@ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core):
103
109
 
104
110
  Earthquake-specific:
105
111
 
106
- - Two independent data sources: USGS ComCat (global, full metadata) and EMSC SeismicPortal (European-Mediterranean, independent catalog)
112
+ - Two independent global data sources: USGS ComCat (full metadata) and EMSC SeismicPortal (an independent catalog from the European-Mediterranean Seismological Centre, with no PAGER/DYFI/ShakeMap metadata and station coverage densest around Europe and the Mediterranean)
107
113
  - USGS real-time GeoJSON feeds (CDN-cached, fast availability) plus FDSN event query API
108
114
  - EMSC FDSN-WS event and count endpoints
109
115
  - No API key required — both USGS and EMSC are fully public
@@ -112,7 +118,10 @@ Agent-friendly output:
112
118
 
113
119
  - Source attribution on every response (`usgs` / `emsc`) so agents can reason about data provenance
114
120
  - `exceeds_limit` flag on count responses surfaces truncation risk before a full search
115
- - USGS-specific fields (`alert_level`, `felt`, `mmi`, `tsunami`) clearly labeled as USGS-only to prevent misattribution on EMSC results
121
+ - Fields a source does not publish come back `null`, never as a fabricated zero — `tsunami` and `status` are null on EMSC events, and the rendered text says "not published by source" rather than "no tsunami" or "reviewed"
122
+ - `source_catalog` and `auth` carry provenance (which catalog and which authoritative agency produced a solution) so agents can weigh two sources against each other
123
+ - USGS-only filters dropped for an EMSC query are named in `ignoredFilters` on both `earthquake_search` and `earthquake_count`
124
+ - An upstream 4xx surfaces the service's own explanation (the offending parameter and its accepted format) in the error message, not just a status code
116
125
 
117
126
  ## Getting started
118
127
 
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ await createApp({
19
19
  instructions: 'Use the earthquake_* tools to query seismic data from USGS ComCat and the EMSC SeismicPortal; no API key required. ' +
20
20
  'earthquake_get_feed gives USGS real-time magnitude tiers for current activity; earthquake_search handles historical or filtered queries, sized first with earthquake_count (search caps at 20,000 events). ' +
21
21
  'Both emit network-specific event IDs (e.g. us6000sznj) for earthquake_get_event, which returns full USGS detail (EMSC has no per-event endpoint). ' +
22
- 'USGS carries richer metadata (PAGER, DYFI, ShakeMap); source=emsc uses the independent European-Mediterranean catalog and ignores USGS-only filters.',
22
+ 'USGS carries richer metadata (PAGER, DYFI, ShakeMap); source=emsc is an independent global catalog from the European-Mediterranean Seismological Centre, useful for cross-checking an event of any region against a separate network, and does not apply USGS-only filters (the response names them in ignoredFilters).',
23
23
  tools: [earthquakeGetFeed, earthquakeSearch, earthquakeGetEvent, earthquakeCount],
24
24
  resources: [earthquakeFeedResource, earthquakeEventResource],
25
25
  prompts: [],
package/dist/index.js.map CHANGED
@@ -1 +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,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,iEAAiE,CAAC;AAC1G,OAAO,EAAE,sBAAsB,EAAE,MAAM,gEAAgE,CAAC;AACxG,OAAO,EAAE,eAAe,EAAE,MAAM,yDAAyD,CAAC;AAC1F,OAAO,EAAE,kBAAkB,EAAE,MAAM,6DAA6D,CAAC;AACjG,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,0DAA0D,CAAC;AAC5F,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAElE,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,uBAAuB;IAC7B,KAAK,EAAE,uBAAuB;IAC9B,YAAY,EACV,qHAAqH;QACrH,6MAA6M;QAC7M,oJAAoJ;QACpJ,sJAAsJ;IACxJ,KAAK,EAAE,CAAC,iBAAiB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,eAAe,CAAC;IACjF,SAAS,EAAE,CAAC,sBAAsB,EAAE,uBAAuB,CAAC;IAC5D,OAAO,EAAE,EAAE;IACX,OAAO,EAAE;QACP,0EAA0E;QAC1E,2FAA2F;QAC3F,WAAW,EAAE,KAAK;KACnB;IACD,KAAK,CAAC,IAAI;QACR,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;QACjC,eAAe,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,gBAAgB,CAAC,CAAC;QACxF,eAAe,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,gBAAgB,CAAC,CAAC;IAC1F,CAAC;CACF,CAAC,CAAC"}
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,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,iEAAiE,CAAC;AAC1G,OAAO,EAAE,sBAAsB,EAAE,MAAM,gEAAgE,CAAC;AACxG,OAAO,EAAE,eAAe,EAAE,MAAM,yDAAyD,CAAC;AAC1F,OAAO,EAAE,kBAAkB,EAAE,MAAM,6DAA6D,CAAC;AACjG,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,0DAA0D,CAAC;AAC5F,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAElE,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,uBAAuB;IAC7B,KAAK,EAAE,uBAAuB;IAC9B,YAAY,EACV,qHAAqH;QACrH,6MAA6M;QAC7M,oJAAoJ;QACpJ,yTAAyT;IAC3T,KAAK,EAAE,CAAC,iBAAiB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,eAAe,CAAC;IACjF,SAAS,EAAE,CAAC,sBAAsB,EAAE,uBAAuB,CAAC;IAC5D,OAAO,EAAE,EAAE;IACX,OAAO,EAAE;QACP,0EAA0E;QAC1E,2FAA2F;QAC3F,WAAW,EAAE,KAAK;KACnB;IACD,KAAK,CAAC,IAAI;QACR,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;QACjC,eAAe,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,gBAAgB,CAAC,CAAC;QACxF,eAAe,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,gBAAgB,CAAC,CAAC;IAC1F,CAAC;CACF,CAAC,CAAC"}
@@ -26,13 +26,15 @@ export declare const earthquakeEventResource: import("@cyanheads/mcp-ts-core").R
26
26
  orange: "orange";
27
27
  red: "red";
28
28
  }>>;
29
- tsunami: z.ZodNumber;
29
+ tsunami: z.ZodNullable<z.ZodNumber>;
30
30
  significance: z.ZodNullable<z.ZodNumber>;
31
- status: z.ZodEnum<{
31
+ status: z.ZodNullable<z.ZodEnum<{
32
32
  automatic: "automatic";
33
33
  reviewed: "reviewed";
34
34
  deleted: "deleted";
35
- }>;
35
+ }>>;
36
+ source_catalog: z.ZodOptional<z.ZodString>;
37
+ auth: z.ZodOptional<z.ZodString>;
36
38
  event_url: z.ZodOptional<z.ZodString>;
37
39
  detail_url: z.ZodOptional<z.ZodString>;
38
40
  }, z.core.$strip>;
@@ -1 +1 @@
1
- {"version":3,"file":"earthquake-event.resource.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/earthquake-event.resource.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAY,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAMrD,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BAkClC,CAAC"}
1
+ {"version":3,"file":"earthquake-event.resource.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/earthquake-event.resource.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAY,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAMrD,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BAkClC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"earthquake-feed.resource.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/earthquake-feed.resource.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAY,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAiBrD,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;6BA8FjC,CAAC"}
1
+ {"version":3,"file":"earthquake-feed.resource.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/earthquake-feed.resource.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAY,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAiBrD,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;6BAiGjC,CAAC"}
@@ -18,7 +18,10 @@ export const earthquakeFeedResource = resource('earthquake://feed/{magnitude_tie
18
18
  title: 'USGS Earthquake Feed',
19
19
  description: 'Direct access to a USGS real-time earthquake feed as injectable context. ' +
20
20
  'magnitude_tier: all | 1.0 | 2.5 | 4.5 | significant. ' +
21
- 'time_window: hour | day | week | month.',
21
+ 'time_window: hour | day | week | month. ' +
22
+ 'Returns the whole feed in one read — the URI carries no paging, so the broad combinations ' +
23
+ '("all" or "1.0" with "week" or "month") can run to thousands of events. ' +
24
+ 'Use the earthquake_get_feed tool for those: it serves the same data a bounded page at a time.',
22
25
  mimeType: 'application/json',
23
26
  params: z.object({
24
27
  magnitude_tier: z.string().describe('Magnitude tier: all, 1.0, 2.5, 4.5, or significant.'),
@@ -1 +1 @@
1
- {"version":3,"file":"earthquake-feed.resource.js","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/earthquake-feed.resource.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"earthquake-feed.resource.js","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/earthquake-feed.resource.ts"],"names":[],"mappings":"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"}
@@ -42,7 +42,39 @@ export declare const earthquakeCount: import("@cyanheads/mcp-ts-core").ToolDefin
42
42
  }, {
43
43
  readonly reason: "source_unavailable";
44
44
  readonly code: JsonRpcErrorCode.ServiceUnavailable;
45
- readonly when: "Selected source API returns non-2xx or times out.";
45
+ readonly when: "Selected source API returns a 5xx or is unreachable.";
46
+ readonly retryable: true;
46
47
  readonly recovery: "Try the other source (usgs or emsc) or retry after a short delay.";
47
- }], undefined>;
48
+ }, {
49
+ readonly reason: "source_timeout";
50
+ readonly code: JsonRpcErrorCode.Timeout;
51
+ readonly when: "Selected source API did not answer before the request deadline.";
52
+ readonly retryable: true;
53
+ readonly recovery: string;
54
+ }, {
55
+ readonly reason: "upstream_rejected";
56
+ readonly code: JsonRpcErrorCode.InvalidParams;
57
+ readonly when: "The source API rejected the query parameters with a 4xx response.";
58
+ readonly recovery: string;
59
+ }], {
60
+ readonly queryEcho: z.ZodOptional<z.ZodObject<{
61
+ start_time: z.ZodOptional<z.ZodString>;
62
+ end_time: z.ZodOptional<z.ZodString>;
63
+ min_magnitude: z.ZodOptional<z.ZodNumber>;
64
+ max_magnitude: z.ZodOptional<z.ZodNumber>;
65
+ latitude: z.ZodOptional<z.ZodNumber>;
66
+ longitude: z.ZodOptional<z.ZodNumber>;
67
+ radius_km: z.ZodOptional<z.ZodNumber>;
68
+ min_depth_km: z.ZodOptional<z.ZodNumber>;
69
+ max_depth_km: z.ZodOptional<z.ZodNumber>;
70
+ alert_level: z.ZodOptional<z.ZodString>;
71
+ min_felt: z.ZodOptional<z.ZodNumber>;
72
+ min_significance: z.ZodOptional<z.ZodNumber>;
73
+ source: z.ZodEnum<{
74
+ usgs: "usgs";
75
+ emsc: "emsc";
76
+ }>;
77
+ }, z.core.$strip>>;
78
+ readonly ignoredFilters: z.ZodOptional<z.ZodArray<z.ZodString>>;
79
+ }>;
48
80
  //# sourceMappingURL=earthquake-count.tool.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"earthquake-count.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/earthquake-count.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAY,MAAM,+BAA+B,CAAC;AAM3E,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAqM1B,CAAC"}
1
+ {"version":3,"file":"earthquake-count.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/earthquake-count.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAY,MAAM,+BAA+B,CAAC;AAO3E,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsV1B,CAAC"}
@@ -4,7 +4,8 @@
4
4
  */
5
5
  import { tool, z } from '@cyanheads/mcp-ts-core';
6
6
  import { JsonRpcErrorCode, McpError } from '@cyanheads/mcp-ts-core/errors';
7
- import { buildQueryParams } from '../../../mcp-server/tools/query-params.js';
7
+ import { upstreamRejection } from '../../../mcp-server/tools/fdsn-error.js';
8
+ import { buildQueryParams, ignoredUsgsFilters } from '../../../mcp-server/tools/query-params.js';
8
9
  import { getEmscService } from '../../../services/emsc/emsc-service.js';
9
10
  import { getUsgsService } from '../../../services/usgs/usgs-service.js';
10
11
  export const earthquakeCount = tool('earthquake_count', {
@@ -12,11 +13,14 @@ export const earthquakeCount = tool('earthquake_count', {
12
13
  description: 'Count earthquakes matching filters without fetching full records. ' +
13
14
  'Use for statistical queries ("how many M5+ earthquakes in 2025?") or to gauge result size ' +
14
15
  'before calling earthquake_search. ' +
16
+ 'Omitting start_time counts only the last 30 days, so pass an explicit range for any ' +
17
+ 'period-specific question; queryEcho reports the window and filters the count actually covers. ' +
15
18
  'When exceeds_limit is true, the count exceeds 20,000 and a full search would be truncated — ' +
16
19
  'narrow filters before fetching. ' +
17
20
  'USGS returns the max_allowed cap (20,000); EMSC count endpoint does not return this field ' +
18
21
  '(max_allowed will be null). ' +
19
- 'USGS-specific filters (alert_level, min_felt, min_significance) are ignored when source=emsc.',
22
+ 'USGS-specific filters (alert_level, min_felt, min_significance) are not sent when source=emsc' +
23
+ 'the response names them in ignoredFilters.',
20
24
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
21
25
  input: z.object({
22
26
  start_time: z
@@ -84,9 +88,12 @@ export const earthquakeCount = tool('earthquake_count', {
84
88
  source: z
85
89
  .enum(['usgs', 'emsc'])
86
90
  .default('usgs')
87
- .describe('Data source. ' +
91
+ .describe('Data source. Both catalogs are global. ' +
88
92
  '"usgs" covers global events with PAGER, DYFI, and ShakeMap metadata. ' +
89
- '"emsc" covers the European-Mediterranean region.'),
93
+ '"emsc" is an independent global catalog operated by the European-Mediterranean ' +
94
+ 'Seismological Centre — use it to cross-check a count from a separate network. ' +
95
+ 'It has no PAGER, DYFI, or ShakeMap metadata; its station coverage is densest around ' +
96
+ 'Europe and the Mediterranean, so counts of small events differ most by region.'),
90
97
  }),
91
98
  output: z.object({
92
99
  count: z.number().describe('Number of events matching the query.'),
@@ -102,6 +109,66 @@ export const earthquakeCount = tool('earthquake_count', {
102
109
  'For EMSC, evaluated against the known 20000 limit since max_allowed is not returned. ' +
103
110
  'Narrow filters to retrieve all matching events.'),
104
111
  }),
112
+ // A count is a single number with no context of its own — the window and filters
113
+ // it covers have to travel with it, including the server-resolved 30-day default.
114
+ // Populated via ctx.enrich(...) so it reaches structuredContent and content[].
115
+ enrichment: {
116
+ queryEcho: z
117
+ .object({
118
+ start_time: z
119
+ .string()
120
+ .optional()
121
+ .describe('Effective query start time sent upstream — server-resolved to a 30-day window when omitted from input.'),
122
+ end_time: z
123
+ .string()
124
+ .optional()
125
+ .describe('Effective query end time. Absent when omitted from input — the upstream defaults to the current time.'),
126
+ min_magnitude: z.number().optional().describe('Minimum magnitude filter sent upstream.'),
127
+ max_magnitude: z.number().optional().describe('Maximum magnitude filter sent upstream.'),
128
+ latitude: z.number().optional().describe('Radius-search latitude sent upstream.'),
129
+ longitude: z.number().optional().describe('Radius-search longitude sent upstream.'),
130
+ radius_km: z
131
+ .number()
132
+ .optional()
133
+ .describe('Search radius in km sent upstream (converted to degrees for EMSC).'),
134
+ min_depth_km: z.number().optional().describe('Minimum depth filter sent upstream.'),
135
+ max_depth_km: z.number().optional().describe('Maximum depth filter sent upstream.'),
136
+ alert_level: z
137
+ .string()
138
+ .optional()
139
+ .describe('PAGER alert filter sent upstream. Absent for EMSC — not supported there.'),
140
+ min_felt: z
141
+ .number()
142
+ .optional()
143
+ .describe('DYFI felt-report filter sent upstream. Absent for EMSC — not supported there.'),
144
+ min_significance: z
145
+ .number()
146
+ .optional()
147
+ .describe('Significance filter sent upstream. Absent for EMSC — not supported there.'),
148
+ source: z.enum(['usgs', 'emsc']).describe('Data source queried.'),
149
+ })
150
+ .optional()
151
+ .describe('Echo of the effective parameters the count covers, including server-resolved defaults. ' +
152
+ 'Read start_time and end_time to know which window the count spans — a filter absent ' +
153
+ 'here was not sent upstream.'),
154
+ ignoredFilters: z
155
+ .array(z.string().describe('Name of an input filter that was not applied.'))
156
+ .optional()
157
+ .describe('USGS-only filters supplied in the input but not sent upstream because source=emsc ' +
158
+ 'does not support them. The count is NOT constrained by these — re-run with ' +
159
+ 'source=usgs to apply them. Absent when every supplied filter was applied.'),
160
+ },
161
+ enrichmentTrailer: {
162
+ queryEcho: {
163
+ render: (q) => `**Query echo:** ${Object.entries(q ?? {})
164
+ .map(([key, value]) => `${key}=${String(value)}`)
165
+ .join(' · ')}`,
166
+ },
167
+ ignoredFilters: {
168
+ render: (f) => `**Ignored filters (not supported by EMSC, not sent upstream):** ${(f ?? []).join(', ')} — ` +
169
+ 'this count is NOT constrained by the listed filters. Re-run with source=usgs to apply them.',
170
+ },
171
+ },
105
172
  errors: [
106
173
  {
107
174
  reason: 'invalid_radius',
@@ -112,9 +179,25 @@ export const earthquakeCount = tool('earthquake_count', {
112
179
  {
113
180
  reason: 'source_unavailable',
114
181
  code: JsonRpcErrorCode.ServiceUnavailable,
115
- when: 'Selected source API returns non-2xx or times out.',
182
+ when: 'Selected source API returns a 5xx or is unreachable.',
183
+ retryable: true,
116
184
  recovery: 'Try the other source (usgs or emsc) or retry after a short delay.',
117
185
  },
186
+ {
187
+ reason: 'source_timeout',
188
+ code: JsonRpcErrorCode.Timeout,
189
+ when: 'Selected source API did not answer before the request deadline.',
190
+ retryable: true,
191
+ recovery: 'Narrow the time range or raise min_magnitude so the upstream count scans a smaller ' +
192
+ 'window, then retry. Counts over multi-year spans are the usual cause.',
193
+ },
194
+ {
195
+ reason: 'upstream_rejected',
196
+ code: JsonRpcErrorCode.InvalidParams,
197
+ when: 'The source API rejected the query parameters with a 4xx response.',
198
+ recovery: 'Read the upstream reason in the error message — it names the offending parameter and ' +
199
+ 'the accepted format. Correct that parameter and call again.',
200
+ },
118
201
  ],
119
202
  async handler(input, ctx) {
120
203
  const latProvided = input.latitude != null;
@@ -144,6 +227,20 @@ export const earthquakeCount = tool('earthquake_count', {
144
227
  ...ctx.recoveryFor('source_unavailable'),
145
228
  });
146
229
  }
230
+ // A timeout classifies as Timeout, not ServiceUnavailable — it needs its own
231
+ // branch or it bypasses the contract and reaches the caller with no recovery hint.
232
+ if (err instanceof McpError && err.code === JsonRpcErrorCode.Timeout) {
233
+ throw ctx.fail('source_timeout', err.message, {
234
+ ...ctx.recoveryFor('source_timeout'),
235
+ });
236
+ }
237
+ // A 4xx means the upstream rejected the parameters, and its body says which one.
238
+ // The framework leaves that body out of the message, so fold it in here — otherwise
239
+ // content[]-only clients see a bare status code.
240
+ const rejection = upstreamRejection(err);
241
+ if (rejection) {
242
+ throw ctx.fail('upstream_rejected', `${input.source.toUpperCase()} rejected the query: ${rejection.reason}`, { ...ctx.recoveryFor('upstream_rejected'), status: rejection.status }, { cause: err });
243
+ }
147
244
  throw err;
148
245
  }
149
246
  ctx.log.info('Count completed', {
@@ -151,6 +248,33 @@ export const earthquakeCount = tool('earthquake_count', {
151
248
  count: result.count,
152
249
  exceeds_limit: result.exceedsLimit,
153
250
  });
251
+ // Echo the effective upstream parameters — USGS-only filters are excluded for
252
+ // EMSC because buildFdsnQuery does not send them.
253
+ const isUsgs = input.source !== 'emsc';
254
+ ctx.enrich({
255
+ queryEcho: {
256
+ ...(params.startTime != null ? { start_time: params.startTime } : {}),
257
+ ...(params.endTime != null ? { end_time: params.endTime } : {}),
258
+ ...(params.minMagnitude != null ? { min_magnitude: params.minMagnitude } : {}),
259
+ ...(params.maxMagnitude != null ? { max_magnitude: params.maxMagnitude } : {}),
260
+ ...(params.latitude != null ? { latitude: params.latitude } : {}),
261
+ ...(params.longitude != null ? { longitude: params.longitude } : {}),
262
+ ...(params.radiusKm != null ? { radius_km: params.radiusKm } : {}),
263
+ ...(params.minDepthKm != null ? { min_depth_km: params.minDepthKm } : {}),
264
+ ...(params.maxDepthKm != null ? { max_depth_km: params.maxDepthKm } : {}),
265
+ ...(isUsgs && params.alertLevel != null ? { alert_level: params.alertLevel } : {}),
266
+ ...(isUsgs && params.minFelt != null ? { min_felt: params.minFelt } : {}),
267
+ ...(isUsgs && params.minSignificance != null
268
+ ? { min_significance: params.minSignificance }
269
+ : {}),
270
+ source: input.source,
271
+ },
272
+ });
273
+ // An absence from queryEcho is too quiet a signal that a supplied filter never
274
+ // constrained the count — name the dropped filters outright.
275
+ const ignoredFilters = ignoredUsgsFilters(input, input.source);
276
+ if (ignoredFilters.length > 0)
277
+ ctx.enrich({ ignoredFilters });
154
278
  return {
155
279
  count: result.count,
156
280
  max_allowed: result.maxAllowed,