@cyanheads/earthquake-mcp-server
Search USGS and EMSC seismic data — real-time feeds, event queries, and earthquake counts via MCP. STDIO or Streamable HTTP.
4 Tools • 3 Resources
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/earthquake-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/earthquake-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
[](https://github.com/cyanheads/earthquake-mcp-server/releases/latest/download/earthquake-mcp-server.mcpb) [](https://cursor.com/en/install-mcp?name=earthquake-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvZWFydGhxdWFrZS1tY3Atc2VydmVyIl19) [](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22earthquake-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads%2Fearthquake-mcp-server%22%5D%7D)
[](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
**Public Hosted Server:** [https://earthquake.caseyjhand.com/mcp](https://earthquake.caseyjhand.com/mcp)
---
## Overview
Seismic data from USGS ComCat and the EMSC SeismicPortal. Fetch real-time earthquake feeds, search and count seismic events by time, magnitude, depth, and location, and pull full analysis detail for a single event.
### Tools
| Tool | Description |
|:---|:---|
| `earthquake_get_feed` | Fetch a USGS pre-computed real-time earthquake feed by magnitude tier and time window |
| `earthquake_search_events` | Search earthquakes by time range, magnitude, depth, location radius, PAGER alert level, or felt reports |
| `earthquake_count_events` | Count earthquakes matching filters without fetching full records |
| `earthquake_get_event` | Fetch one earthquake by USGS or EMSC event ID, with USGS analysis products and opt-in origins, magnitudes, phase arrivals, and aftershock forecast |
### Resources
| Resource | Description |
|:---|:---|
| `earthquake://feed/{magnitude_tier}/{time_window}` | First 100-event page of a USGS real-time feed, with an exact continuation URI |
| `earthquake://feed/{magnitude_tier}/{time_window}/page/{cursor}` | Continue the retained feed generation using the returned `next_uri` |
| `earthquake://event/{event_id}` | One earthquake by USGS or EMSC event ID as injectable context, with the same `detail` product projection as `earthquake_get_event` |
### Skill
| Skill | Description |
|:---|:---|
| [`earthquake-guide`](skills/earthquake-guide/SKILL.md) | Agent guide for the `earthquake_*` tools: identifier chains between USGS and EMSC, coverage and magnitude-scale gates, and when product and `include` groups answer a question. Loaded by plugin hosts from `skills/` and served over MCP as `skill://earthquake-guide/SKILL.md` |
## Capability reference
### `earthquake_get_feed` tool
- Choose a magnitude tier (`all`, `1.0`, `2.5`, `4.5`, `significant`) and a window (`hour`, `day`, `week`, `month`). USGS caches these feeds; use `earthquake_search_events` for historical or filtered queries.
- Returns events, page count, generation time, and source feed URL. `all` includes microseisms; `significant` is USGS-curated using magnitude, felt reports, and PAGER impact.
- Each page returns `limit` events (default 15), fewer only on the last page. A `limit` above 1,000 returns 1,000 with a notice naming the requested and applied values; with `EARTHQUAKE_CLAMP_LIMITS=false` it is rejected instead. `limit` applies to the first page; later pages keep the page size in the cursor. `totalCount` reports the whole feed; pass `nextCursor` back unchanged with the same tier and window.
- Continuations read the first-read generation, unaffected by upstream edits. Retention lasts up to 15 minutes, subject to capacity eviction and restart. `cursor_expired` means restart without a cursor; legacy cursors must also be restarted.
---
### `earthquake_search_events` tool
- Search `usgs` or `emsc` by time, magnitude, depth, radius, or bounding box. A radius requires `latitude`, `longitude`, and `radius_km` together; box edges are independently optional, support antimeridian bounds up to ±360°, and intersect a supplied circle.
- Returns normalized events with `event_type`, EMSC's `event_certainty`, and USGS's `associated_ids` (other network IDs merged into the event). `ignoredFilters` names unsupported filters; `queryEcho` reports the effective query.
- Both search and count accept open `magnitude_type` values on either source. `product_type` (required product, e.g. `moment-tensor`) and `review_status` (`automatic` or `reviewed`) apply only to USGS; EMSC names them in `ignoredFilters`. Blank values mean omitted.
- Date-only bounds (including expanded year/year-month forms) mean midnight at the start of the named day on both sources: `2026-10-10` becomes `2026-10-10T00:00:00`. Explicit time and timezone suffixes are preserved; unpadded date components are padded. Offsetless times use UTC for the 30-day default. `queryEcho` shows the exact effective bounds.
- Sort by time or magnitude in either direction. Each call requests `limit` events upstream (default 15, set by `DEFAULT_LIMIT`) and returns them all, fewer only on the last page. A `limit` above 1,000 returns 1,000 with a notice naming the requested and applied values; with `EARTHQUAKE_CLAMP_LIMITS=false` it is rejected instead. `queryEcho.limit` reports the applied limit. Continue with the returned `nextOffset` (offset plus the events returned, counting from 1) and the same filters, limit, and order. Search pages read the current catalog, so upstream edits can affect later pages.
- Strictly inverted magnitude, depth, or explicit time bounds fail locally as `invalid_range`, naming both values and recovery. Equal and single bounds remain valid; time comparisons use UTC instants, including explicit offsets. Calendar-invalid values remain for upstream rejection.
- A nonempty short page establishes `totalCount`; an empty first page establishes zero, while an empty later page leaves the total unknown. Full pages use a count lookup, with `countUnavailable` on failure. Feed, search, and event calls each share one 50-second deadline across their requests and retries; for search that includes the count lookup. Cancellation remains cancellation.
---
### `earthquake_count_events` tool
- Count matches using the same filters as `earthquake_search_events`, without fetching events. Omit `start_time` for the last 30 days; `queryEcho` reports the resolved window and applied filters.
- `exceeds_limit` flags the 20,000-event single-request threshold, not a retrieval ceiling: search offset paging can retrieve larger match sets. `max_allowed` is 20,000 for USGS and null for EMSC; `ignoredFilters` names filters the source cannot apply.
---
### `earthquake_get_event` tool
- Pass the `id` of any feed or search result. An EMSC unid from a `source=emsc` search (eight digits, underscore, seven digits, e.g. `20190706_0000043`) is looked up on EMSC SeismicPortal; every other ID goes to USGS (e.g. `us6000sznj`).
- A USGS event returns the normalized event plus optional `detail`: PAGER, ShakeMap, DYFI, moment tensor, ground-failure alerts, origin quality (including `depth_type`, which separates an assigned depth from a located one), and finite-fault dimensions. Groups are omitted when none of their projected fields is available; absence does not imply zero impact. `associated_ids` lists the other network IDs USGS holds for the event; an alias such as `us70004bn0` returns the preferred event (`ci38457511`) with the alias listed there.
- An EMSC event returns the same normalized event its search result carries, with no `detail` and no `associated_ids`.
- `include` opts into more for USGS events:
| Group | Source | Extra requests |
|:---|:---|:---|
| `origins` | Every `origin` product in the event response, preferred first | None |
| `magnitudes` | Event-level magnitudes from the preferred `phase-data` product's QuakeML, with a station count where published | One, shared with `arrivals` |
| `arrivals` | Every phase arrival of that document's preferred origin, joined to its pick and sorted nearest first | One, shared with `magnitudes` |
| `aftershock_forecast` | The USGS Operational Aftershock Forecast: each window's probability of an aftershock at or above each magnitude, its expected-count range, and the chance of one as large as the mainshock | One |
- A requested group that is not returned is listed in `unavailable_includes`: `not_published` when the event has no such product (every group on an EMSC event), `fetch_failed` when the download failed, returned HTML, did not parse, pointed outside the configured USGS origin, or answered with a redirect. The event, `detail`, and other groups still return; cancellation propagates. The event request and its product downloads share one 50-second deadline.
- Blank IDs fail locally; canonical IDs and aliases match case-insensitively. ShakeMap includes the producer's `code_version` when published.
---
### `earthquake://feed/{magnitude_tier}/{time_window}` resource
- Choose the same magnitude tiers and time windows as `earthquake_get_feed`; all 20 combinations are listed as browsable resources.
- Returns 100 events per `application/json` page in the compact event projection, plus `totalCount`, `truncated`, `nextCursor`, and `next_uri`. Read `next_uri` verbatim until it is absent. The continuation URI is `earthquake://feed/{magnitude_tier}/{time_window}/page/{cursor}`.
- Feed resources share retained generations with `earthquake_get_feed`. Cache hints are private with zero lifetime because continuation tokens belong to the requesting tenant. Upstream failures classified as `ServiceUnavailable` expose `feed_unavailable`; those classified as `Timeout` expose `feed_timeout`, each with recovery guidance and no captured upstream body. Availability failures preserve numeric HTTP status; HTTP 501 remains non-retryable and points to EMSC search. Other error classes propagate unchanged.
---
### `earthquake://event/{event_id}` resource
- Pass a USGS or EMSC `event_id` from a feed or search result. Returns the same event and optional `detail` products as `earthquake_get_event`; it takes no `include`.
- Uses that tool's `event_deleted`, `not_found`, `source_unavailable`, and `source_timeout` error contract.
## Features
Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): stdio and Streamable HTTP transports, pluggable auth (`none` / `jwt` / `oauth`), swappable storage (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`), structured logging with optional OpenTelemetry tracing.
USGS/EMSC-specific:
- Type-safe clients for the USGS FDSN/GeoJSON API and the EMSC FDSN-WS API, normalizing both into one shared earthquake domain schema
- Automatic retry with backoff and per-request timeouts on every upstream call; detects USGS's rate-limited/CDN failure mode (HTML served instead of GeoJSON) and maps it to a typed service-unavailable error instead of parsing it as data
- EMSC's two-character `evtype` code is decoded against the published event-type/certainty nomenclature into the same vocabulary USGS publishes, so `event_type` carries one meaning across both sources
- USGS-only filters (`alert_level`, `min_felt`, `min_significance`, `event_type`, `product_type`, `review_status`) are named in `ignoredFilters` when `source=emsc`. On USGS, `event_type="earthquake"` excludes quarry blasts and other non-tectonic records.
- No API key or rate-limit tier required — both USGS and EMSC are fully public, keyless APIs
Agent-friendly output:
- Provenance — `source: "usgs" | "emsc"` on search and count responses, plus `source_catalog`/`auth` fields naming the catalog and authoritative agency, so agents can weigh two independent solutions against each other
- Discriminated output contracts — `event_type` and EMSC's `event_certainty` preserve published classifications, with absent fields remaining unknown; `exceeds_limit`, `countUnavailable`, and `truncated` flags let callers branch on data instead of parsing prose
- Response shaping — fields a source does not publish come back `null`, never a fabricated zero (`tsunami`, `status`, and `mmi` are always null on EMSC events); USGS-only filters dropped for an EMSC query are named in `ignoredFilters` rather than silently ignored
- Graceful degradation — an upstream rejection surfaces the service's own explanation (offending parameter, accepted format) in the error message instead of a bare status code, a failed follow-up count degrades to `countUnavailable` rather than failing the whole search, and a failed product download lists its `include` groups as `fetch_failed` rather than failing the event lookup
## Getting started
### Public Hosted Instance
A public instance is available at `https://earthquake.caseyjhand.com/mcp` — no installation required. Point any MCP client at it via Streamable HTTP:
```json
{
"mcpServers": {
"earthquake-mcp-server": {
"type": "streamable-http",
"url": "https://earthquake.caseyjhand.com/mcp"
}
}
}
```
### Self-Hosted / Local
Add the following to your MCP client configuration file.
```json
{
"mcpServers": {
"earthquake-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/earthquake-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
```
Or with npx (no Bun required):
```json
{
"mcpServers": {
"earthquake-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/earthquake-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
```
Or with Docker:
```json
{
"mcpServers": {
"earthquake-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/earthquake-mcp-server:latest"]
}
}
}
```
For Streamable HTTP, set the transport and start the server:
```sh
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
```
### Prerequisites
- [Bun v1.4.0](https://bun.sh/) or higher.
- No API keys required — USGS and EMSC data is fully public.
### Installation
1. **Clone the repository:**
```sh
git clone https://github.com/cyanheads/earthquake-mcp-server.git
```
2. **Navigate into the directory:**
```sh
cd earthquake-mcp-server
```
3. **Install dependencies:**
```sh
bun install
```
## Configuration
All configuration is validated at startup via Zod schemas in `src/config/server-config.ts`. Key environment variables:
| Variable | Description | Default |
|:---|:---|:---|
| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http` | `stdio` |
| `MCP_HTTP_PORT` | HTTP server port | `3010` |
| `MCP_HTTP_ENDPOINT_PATH` | HTTP endpoint path where the MCP server is mounted | `/mcp` |
| `MCP_PUBLIC_URL` | Public origin override for TLS-terminating reverse-proxy deployments | none |
| `MCP_SESSION_MODE` | HTTP sessions: `stateful`, `stateless`, or `auto` (resolves to stateful). Overrides the `createApp()` declaration; Docker and `.env.example` also pin stateless. | `stateless` (declared in `src/index.ts`) |
| `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth` | `none` |
| `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.) | `info` |
| `MCP_GC_PRESSURE_INTERVAL_MS` | Opt-in Bun-only forced-GC pressure loop (ms). Try `60000` if heap growth is observed under sustained HTTP load. | `0` (disabled) |
| `LOGS_DIR` | Directory for log files on Node.js and Bun | `/logs` |
| `LOG_TOOL_FAILURE_PAYLOADS` | Log failed tool arguments and results, redacted by key name. Secrets inside free-form values remain. | `false` |
| `LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES` | UTF-8 byte cap per logged failure payload | `16384` |
| `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1` | `in-memory` |
| `USGS_BASE_URL` | USGS API base URL. Override for testing or mirroring. Product files (phase-data QuakeML, aftershock forecast) are fetched only on this origin, so a mirror must also serve them, with content URLs rewritten to its origin; otherwise those `include` groups report `fetch_failed`. | `https://earthquake.usgs.gov` |
| `EMSC_BASE_URL` | EMSC API base URL. Override for testing or mirroring. | `https://www.seismicportal.eu` |
| `DEFAULT_LIMIT` | Events `earthquake_search_events` returns when a call omits `limit` (1–1,000) | `15` |
| `EARTHQUAKE_CLAMP_LIMITS` | Clamp a search or feed `limit` above 1,000 to 1,000 with a notice. `false` advertises and enforces a schema maximum of 1,000 instead. | `true` |
| `REQUEST_TIMEOUT_MS` | Per-attempt HTTP timeout in milliseconds (1,000–120,000). Feed, search, and event calls also share a 50-second total deadline across every request and retry; standalone count calls use this per-attempt value. | `10000` |
| `OTEL_ENABLED` | Enable OpenTelemetry | `false` |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Base URL for traces (`/v1/traces`) and metrics (`/v1/metrics`); signal-specific endpoint variables override it | none |
| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Opt-in OTLP log endpoint, used as-is; the base URL never enables logs | none |
Empty values and unsubstituted whole-value `${…}` placeholders use the defaults. See [`.env.example`](./.env.example) for optional overrides.
## Running the server
### Local development
- **Build and run the production version**:
```sh
# One-time build
bun run rebuild
# Run the built server
bun run start:http
# or
bun run start:stdio
```
- **Run checks and tests**:
```sh
bun run devcheck # Lints, formats, type-checks, and more
bun run test # Runs the test suite
```
### Docker
```sh
docker build -t earthquake-mcp-server .
docker run --rm -p 3010:3010 earthquake-mcp-server
```
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `/var/log/earthquake-mcp-server`. OpenTelemetry peer dependencies are installed by default — build with `--build-arg OTEL_ENABLED=false` to omit them.
## Project structure
| Directory | Purpose |
|:---|:---|
| `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). Four tools across USGS and EMSC. |
| `src/mcp-server/resources` | Feed, feed continuation, and event resources. |
| `src/services/feed-snapshots.ts` | Tenant-scoped serialized feed retention with bounded payload and metadata accounting. |
| `src/services/usgs` | USGS ComCat service — GeoJSON feed fetcher, FDSN query API client, and the event `include` projections (origin products, phase-data QuakeML, aftershock forecast). |
| `src/services/emsc` | EMSC SeismicPortal service — FDSN event search, count, and by-ID lookup. |
| `src/config` | Server-specific environment variable parsing and validation with Zod. |
| `tests/` | Unit and integration tests, mirroring the `src/` structure. |
## Development guide
See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no `try/catch` in tool logic
- Use `ctx.log` for logging, `ctx.state` for storage
- Register new tools and resources in the `createApp()` arrays
- Validate upstream data, normalize to domain types, and preserve missing values rather than inventing facts
## Contributing
Issues are welcome. Run checks and tests before submitting:
```sh
bun run devcheck
bun run test
```
## License
This project is licensed under the Apache 2.0 License. See the [LICENSE](./LICENSE) file for details.