Skip to main content
Glama
cyanheads

first-epss-mcp-server

by cyanheads
README.md
<div align="center">
  <h1>@cyanheads/first-epss-mcp-server</h1>
  <p><b>Look up FIRST EPSS exploitation scores and daily history, find CVEs by score threshold, and prioritize CVEs with caller-supplied CVSS via MCP. STDIO or Streamable HTTP.</b>
  <div>5 Tools</div>
  </p>
</div>

<div align="center">

[![Version](https://img.shields.io/badge/Version-0.1.2-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/first-epss-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.3.1-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/first-epss-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/first-epss-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.3-blueviolet.svg?style=flat-square)](https://bun.sh/)

</div>

<div align="center">

[![Install in Claude Desktop](https://img.shields.io/badge/Install_in-Claude_Desktop-D97757?style=for-the-badge&logo=anthropic&logoColor=white)](https://github.com/cyanheads/first-epss-mcp-server/releases/latest/download/first-epss-mcp-server.mcpb) [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=first-epss-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvZmlyc3QtZXBzcy1tY3Atc2VydmVyIl19) [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=for-the-badge&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22first-epss-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads%2Ffirst-epss-mcp-server%22%5D%7D)

[![Framework](https://img.shields.io/badge/Built%20on-@cyanheads/mcp--ts--core-67E8F9?style=flat-square)](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)

</div>

---

## Overview

Daily forecasts of observed CVE exploitation within the next 30 days from [FIRST EPSS](https://www.first.org/epss/), generated by Empirical Security and published by FIRST. Look up scores, inspect daily movement, discover CVEs by score or year, and combine likelihood with caller-supplied CVSS severity. The [public API](https://api.first.org/epss/) needs no key or registration.

### Tools

| Tool | Description |
|:---|:---|
| `epss_get_scores` | Fetch current or historical EPSS probability and percentile for supplied CVEs |
| `epss_get_timeseries` | Inspect daily history, exact-date changes, peaks, gaps, and model transitions |
| `epss_query_threshold` | Discover CVEs by inclusive score bounds, CVE year, or recent dataset addition |
| `epss_prioritize_cves` | Assign configurable remediation tiers using EPSS and caller-supplied CVSS severity |
| `epss_list_reference` | Explain score interpretation, models, coverage, and prioritization conventions |

## Capability reference

### `epss_get_scores` <sub>tool</sub>

- Up to 5,000 `cves` as an array or a whitespace/comma/semicolon-delimited string; case and Unicode dashes are normalized. Process up to 500 unique identifiers from `batchOffset` (default 0). Optional `date` (`2021-04-14` through today, UTC) selects a historical snapshot; omitted or blank means latest available
- Each score carries `epss`, `percentile`, `date`, and `modelVersionSource` (`csv_header`, `published_schedule`, or `unavailable`), ordered by EPSS descending
- `unscoredCves` separates `invalid_format` from `not_returned`; a missing score is unknown, never zero
- `snapshot_unavailable` identifies a dated snapshot FIRST cannot supply; select another date or omit it. A CVE omitted from a published snapshot remains `not_returned`

---

### `epss_get_timeseries` <sub>tool</sub>

- Same `cves` / `date` / `batchOffset` inputs; process up to 50 unique identifiers per call
- Each item pairs the anchor `current` with up to 30 preceding daily observations in `series`, plus `missingDates`, `peak`, and `delta7d` / `delta30d`, which need an exact calendar baseline and otherwise appear in `unavailableDeltas`
- `modelBoundariesCrossed` and each comparison's `crossesModelBoundary` flag published model transitions; neither explains why a score moved
- Derived deltas are rounded to nine decimals; original scores retain their precision. Re-anchor on `previousAnchorDate` for the preceding window without overlap; the pointer does not guarantee a published snapshot

---

### `epss_query_threshold` <sub>tool</sub>

- Inclusive `minEpss` / `maxEpss` / `minPercentile` / `maxPercentile` bounds, `year`, and `addedWithinDays` intersect, with optional historical `date`; at least one filter and ordered bounds are required, otherwise `invalid_filter_combination`. `addedWithinDays` is dataset-addition recency and ignores `date`
- One candidate page per call in EPSS descending order: `limit` 1–10,000 (default 100) plus `offset`. With `year`, `candidateTotal` counts broad candidates and `total` is `null` (`totalRelation: "unknown"`), so a filtered page can be empty with `hasMore: true`
- Complete score rows fit within the 65,536-byte limit on each serialized response surface. When rows are withheld, `pagination.budgetLimited` is true and `withheldCount` counts inspected matches omitted; `examinedCount` still counts every inspected candidate. `nextOffset` resumes at the first withheld match's original candidate position, including with year filtering
- Continue with the same filters and `limit`, `provenance.scoreDates[0]` as `date`, and `nextOffset` as `offset`

---

### `epss_prioritize_cves` <sub>tool</sub>

- Up to 5,000 `{ cve, cvssBaseScore? }` entries (CVSS 0–10, caller-supplied) against latest or optional dated EPSS; process up to 500 unique CVEs from `batchOffset`. A duplicate CVE with differing CVSS, including missing versus supplied, fails as `conflicting_cvss`. Unavailable dated snapshots fail as `snapshot_unavailable`
- Defaults `severityThreshold: 7` and `likelihood: { metric: "epss", threshold: 0.10 }`, both inclusive; `metric: "percentile"` is an explicit alternative, and neither is FIRST guidance
- Tiers are `act_now` (high severity, high likelihood), `scheduled` (high, low), `monitor` (low, high), and `defer` (low, low); missing CVSS lands in `epssOnly`, missing EPSS in `unscoredCves` with its CVSS kept

---

### `epss_list_reference` <sub>tool</sub>

- `topic`: `interpretation`, `models`, `coverage`, `prioritization`, or `all` (default)
- Static, source-linked guidance with a `verifiedAt` date; makes no upstream request

### Batch continuation

All four dynamic tools bound each JSON-serialized `content` array and `structuredContent` object to 65,536 UTF-8 bytes, including notices, provenance, filters and pagination. Threshold discovery uses candidate continuation as described above; supplied-CVE tools use the batch continuation below.

Scores, timeseries and prioritization retain whole score rows, history items and unscored entries. `processedCount` still counts the workload processed, including invalid identifiers; `returnedCount` counts complete entries retained and `withheldCount` counts processed entries omitted by the byte limit. `deferredCount` counts entries never queried. `withheldCves` and `deferredCves` retain their array types; their `*Complete` flags explicitly identify previews when the full lists cannot fit. `budgetLimited` discloses any response shortening.

When `nextBatchOffset` is present, call the same tool with the **original** `cves`, `nextBatchOffset` as `batchOffset`, and `provenance.scoreDates[0]` as `date` (retain the requested date if no score date was returned). Prioritization also keeps every original `cvssBaseScore`, `severityThreshold` and `likelihood`. The offset counts normalized unique identifiers in first-occurrence input order, including invalid entries; score sorting does not change it. Continue until `nextBatchOffset` is absent. An offset past the end returns explicit empty accounting. Lists are for inspection; continuation through the original batch preserves entries even when lists are previews.

## 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.

FIRST EPSS-specific:

- Keyless lookup against FIRST's public API, with bounded batches rather than corpus synchronization
- Shared API request pacing, transient-failure retries, cancellation, and a 45-second whole-call deadline
- Model attribution from a matching daily CSV header or the verified published schedule for explicitly dated requests and preceding history; current-anchor verification failures remain visible

Agent-friendly output:

- Provenance: API source, retrieval time, score dates, and model metadata marked `verified`, `inferred`, or `unavailable`
- Explicit coverage: missing EPSS and CVSS remain unknown; `unscoredCves` and `deferredCves` distinguish omissions from work not queried. Transport failures fail the call
- Structured results and formatted text carry the same accounting, history gaps, pagination, thresholds, and recovery guidance

## Getting started

Add the following to your MCP client configuration file.

```json
{
  "mcpServers": {
    "first-epss-mcp-server": {
      "type": "stdio",
      "command": "bunx",
      "args": ["@cyanheads/first-epss-mcp-server@latest"],
      "env": {
        "MCP_TRANSPORT_TYPE": "stdio",
        "MCP_LOG_LEVEL": "info"
      }
    }
  }
}
```

Or with npx (no Bun required):

```json
{
  "mcpServers": {
    "first-epss-mcp-server": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@cyanheads/first-epss-mcp-server@latest"],
      "env": {
        "MCP_TRANSPORT_TYPE": "stdio",
        "MCP_LOG_LEVEL": "info"
      }
    }
  }
}
```

Or with Docker:

```json
{
  "mcpServers": {
    "first-epss-mcp-server": {
      "type": "stdio",
      "command": "docker",
      "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/first-epss-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 (or Node.js v24+).
- Internet access to FIRST's public EPSS API; no API key or account is required.

### Installation

1. **Clone the repository:**

```sh
git clone https://github.com/cyanheads/first-epss-mcp-server.git
```

2. **Navigate into the directory:**

```sh
cd first-epss-mcp-server
```

3. **Install dependencies:**

```sh
bun install
```

4. **Configure environment:**

```sh
cp .env.example .env
# Optional: edit framework transport, auth, logging, and telemetry settings
```

## Configuration

No server-specific settings are required.

| Variable | Description | Default |
|:---|:---|:---|
| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
| `MCP_HTTP_HOST` | HTTP bind address. | `127.0.0.1` |
| `MCP_HTTP_PORT` | HTTP server port. | `3010` |
| `MCP_HTTP_ENDPOINT_PATH` | HTTP MCP endpoint path. | `/mcp` |
| `MCP_SESSION_MODE` | HTTP session mode: `stateless`, `stateful`, or `auto`. | `auto`; `.env.example` and Docker select `stateless` |
| `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth`. | `none` |
| `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.). | `debug`; client examples and Docker select `info` |
| `LOGS_DIR` | Directory for log files (Node.js only). | `<app-root>/logs` |
| `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1`. | `in-memory` |
| `OTEL_ENABLED` | Enable [OpenTelemetry](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry). | `false` |

See [`.env.example`](./.env.example) for every server setting and the common framework 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
  ```

## Project structure

| Directory | Purpose |
|:---|:---|
| `src/mcp-server/tools` | Five tool definitions, shared schemas, formatting, and history policy. |
| `src/services/epss` | FIRST API requests, normalization, pacing, retries, and model metadata. |
| `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
- Export new tools from the `src/mcp-server/tools/definitions/index.ts` barrel, which `src/index.ts` passes to `createApp()`
- Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields

## 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.