echelongraph-mcp 2.4.0 → 2.5.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,142 @@
1
+ # Changelog
2
+
3
+ Every version of `echelongraph-mcp` published to npm, newest first. Dates are the npm publish
4
+ dates (UTC). Issue numbers refer to EchelonGraph's tracker. Since 2.3.3 each version is published
5
+ with a provenance attestation by the release workflow of
6
+ [github.com/echelongraph/echelongraph-mcp](https://github.com/echelongraph/echelongraph-mcp),
7
+ from the commit tagged `v<version>`.
8
+
9
+ ## 2.5.1 — 2026-10-04
10
+
11
+ - `search_cves` sends its search text in the `X-EG-Search` request header instead of the URL
12
+ query string, so the text is kept out of request URLs, which the hosting platform's tracing
13
+ records (#1983). Severity, minimum CVSS, sort and limit stay in the query string.
14
+
15
+ ## 2.5.0 — 2026-10-04
16
+
17
+ - Four prompts (`triage_cve`, `kev_weekly_brief`, `am_i_affected`, `sbom_review`) and three
18
+ resources (`echelongraph://methodology`, `echelongraph://sources`, `cve://{cve_id}`), in both
19
+ protocol eras (#2722).
20
+ - README: a quick start for Claude Desktop, Claude Code, Cursor, VS Code, Windsurf and Cline,
21
+ and for the hosted endpoint; a title and an example question per tool; what is sent where;
22
+ the API's and the hosted endpoint's rate limits; the HTTP entrypoint's settings.
23
+ - This changelog, now in the npm package.
24
+ - `server.json`: a description covering the 14 tools, and the icon.
25
+
26
+ ## 2.4.0 — 2026-10-04
27
+
28
+ Nine new tools, 14 in all (#2726):
29
+
30
+ - `kev_recent`: the CVEs CISA has added to its KEV catalog, newest first, with due date,
31
+ ransomware use, EPSS and when EchelonGraph first saw each in the catalog; filter by date
32
+ range, ransomware and vendor, and page with a cursor (#2717).
33
+ - `epss_history`: how one CVE's EPSS score has changed, as EchelonGraph recorded it, change-only
34
+ and never a daily series (#2718).
35
+ - `check_affected`: whether a product (CPE path) or a registry package at a version is affected
36
+ by known CVEs; `assessed` false is never "not affected" (#2716).
37
+ - `check_sbom`: up to 200 purls, or a CycloneDX or SPDX JSON document read on your machine,
38
+ checked against the advisory corpus, one verdict per component (#2721).
39
+ - `cve_intel`: weakness (CWE), public exploit code, affected packages and fixed versions for one
40
+ CVE (#2720).
41
+ - `get_cwe`: one CWE and the CVEs classified under it, CISA-KEV-listed first (#2720).
42
+ - `vendor_advisories_for_cve`, `get_vendor_advisory`, `search_vendor_advisories`: the
43
+ vendor-published advisories that name a CVE, one advisory in full, and search (#2719).
44
+
45
+ Also:
46
+
47
+ - An HTTP entrypoint, `dist/http.js` (`npm run start:http`): the same tools over stateless
48
+ Streamable HTTP on `POST /mcp`, in both protocol eras, with an Origin policy, a per-client limit
49
+ and a body cap. It is what EchelonGraph's hosted endpoint, `https://mcp.echelongraph.io/mcp`,
50
+ runs (#2316).
51
+ - `ECHELONGRAPH_MCP_UA`: one product token put ahead of the package's User-Agent, so an
52
+ automated caller such as a monitor is told apart from people using the server (#2724).
53
+ - Inputs a user types for `check_affected`, `kev_recent` and `search_vendor_advisories` travel in
54
+ `X-EG-*` request headers, never in the URL (#1983).
55
+ - `get_cve` relays NVD's configuration tree (`cpe_configurations`) when the API serves it, and
56
+ says when it does not; a missing tree does not mean no product is affected (#2720).
57
+ - A failure message names the HTTP method of a request that is not a GET (`check_sbom`'s POST).
58
+ - Listing files for directories (`glama.json`, a `Dockerfile`, an MCPB manifest); none of them is
59
+ in the npm package (#2723).
60
+
61
+ ## 2.3.4 — 2026-09-30
62
+
63
+ - `cve_summary` says what `summary.nvd_critical` … `summary.nvd_none` count (the same active
64
+ CVEs by NVD's severity label, as provenance, never EchelonGraph's severity) and what
65
+ `summary.rejected` counts (withdrawn records, outside the total) (#2641).
66
+
67
+ ## 2.3.3 — 2026-09-30
68
+
69
+ - The first release published by the public repo's release workflow, with npm provenance. No
70
+ change to the server beyond the version (#2317).
71
+
72
+ ## 2.3.2 — 2026-09-30
73
+
74
+ - `cve_summary`: `summary.none` is labelled as CVEs not yet scored, never as a severity rating of
75
+ None (#2610).
76
+
77
+ ## 2.3.1 — 2026-09-30
78
+
79
+ - `get_cve` and `search_cves` label a CVE EchelonGraph has not scored NOT YET SCORED (a rejected
80
+ record NOT SCORED), never as an EchelonGraph score of 0 (#2535).
81
+
82
+ ## 2.3.0 — 2026-09-30
83
+
84
+ - `exposure_radar` reads a fifth radar, `mcp_servers`: hostnames named like an MCP server in
85
+ EchelonGraph's own Certificate Transparency feed, by their latest verdict on record (RFC 9728
86
+ protected, pending re-adjudication, or not assessed by reason), with the protocol era and
87
+ transport splits. An answer that does not say `service` `mcp` is refused, never relayed (#2315).
88
+
89
+ ## 2.2.0 — 2026-09-28
90
+
91
+ - `not_assessed` no longer says its numbers are not findings: a count it relays is what the
92
+ source holds on record, undated (#2465).
93
+ - The last text block stops repeating what an earlier block already says (#2467).
94
+
95
+ ## 2.1.0 — 2026-09-27
96
+
97
+ - `cve_exposure` answers `not_assessed`, with `measured_at` null: its `last_seen` is when
98
+ EchelonGraph last wrote a row, not when a service was observed (#2439).
99
+ - Every result's last text block carries the structured result, for clients that pass only
100
+ `content` to the model (#2440).
101
+
102
+ ## 2.0.0 — 2026-09-27
103
+
104
+ - Both MCP protocol eras on stdio: `server/discover` (2026-07-28) and `initialize` (2025-11-25
105
+ and earlier) (#2311).
106
+ - Every tool has a title, annotations (`readOnlyHint` and the rest) and an `outputSchema`; every
107
+ result carries `structuredContent` with `state`, `measured_at`, `method`, `coverage`,
108
+ `freshness` and `notes` (#2313).
109
+ - `@modelcontextprotocol/server` 2.1.0 replaces `@modelcontextprotocol/sdk` 1.x; Node.js 20 or
110
+ later (Node.js 18 dropped).
111
+
112
+ ## 1.0.4 — 2026-09-27
113
+
114
+ - `exposure_radar` labels every number of every radar by what it counts, leaves out and names a
115
+ field it cannot label, and never relays a CVE the radar does not track as "0 exposed" (#2313).
116
+ - `exposure_radar` relays each radar's `last_run_at`, its last completed check (#2335).
117
+ - The MIT `LICENSE` file ships in the package.
118
+
119
+ ## 1.0.3 — 2026-09-27
120
+
121
+ - `exposure_radar` regroups the shadow-AI radar's numbers by what each counts
122
+ (`confirmed_exposed`, `observed`, `authentication`) and relays no number unlabelled (#2307).
123
+
124
+ ## 1.0.2 — 2026-09-27
125
+
126
+ - Freshness is the radar's last completed cycle, not a follower instance's zero time (#2307).
127
+ - Every Shodan-derived field states Shodan's ownership and copyright (#2306).
128
+
129
+ ## 1.0.1 — 2026-09-27
130
+
131
+ - An API failure is an error result, never a success with empty data (#1874).
132
+ - Wording: no claim the code does not support; exposure counts are internet-facing services
133
+ (distinct ip:port), attributed as derived from Shodan data (#2306).
134
+ - `cve_exposure` refuses an id that is not a CVE ID, and reports a CVE outside the radar's
135
+ tracked set as not assessed, not 0 (#2313).
136
+ - `mcpName` and `server.json` for the official MCP Registry (#1879); the handshake and the
137
+ User-Agent carry the package version.
138
+
139
+ ## 1.0.0 — 2026-06-25
140
+
141
+ - First release: `cve_summary`, `search_cves`, `get_cve`, `cve_exposure` and `exposure_radar`
142
+ over stdio.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # EchelonGraph MCP server
2
2
 
3
- **CVE and internet-exposure data** for Claude, Cursor, Cline, and any
3
+ **CVE and internet-exposure data** for Claude, Cursor, VS Code, Cline, and any
4
4
  [MCP](https://modelcontextprotocol.io) client, straight from
5
5
  [EchelonGraph](https://echelongraph.io)'s free public feed.
6
6
 
@@ -10,27 +10,164 @@ internet-facing services (distinct ip:port) EchelonGraph's KEV-exposure radar ha
10
10
  running a version that maps to the CVE. Exposure counts are derived from Shodan data.
11
11
  Shodan data is owned by Shodan, which holds its copyright (© Shodan).
12
12
 
13
+ Its 14 tools look up one CVE (record, exploit code and fixed versions, EPSS history, vendor
14
+ advisories, exposure footprint), list CISA's newest KEV additions, check whether a product or
15
+ package version is affected, check an SBOM, and read a CWE and its CVEs. Every result says how
16
+ it was measured (`state`, `measured_at`, `method`, `coverage`, `freshness`, `notes`), so a model
17
+ cannot mistake an outage or an unassessed lookup for an all-clear.
18
+
19
+ It also serves four prompts, ready-made workflows a client can show as slash commands
20
+ (`triage_cve`, `kev_weekly_brief`, `am_i_affected`, `sbom_review`; see "Prompts"), and three
21
+ resources (`echelongraph://methodology`, `echelongraph://sources` and `cve://{cve_id}`; see
22
+ "Resources").
23
+
13
24
  Free and keyless: no API key, no auth, read-only. The server makes no request other than the
14
25
  API call a tool needs to answer.
15
26
 
27
+ - npm: [`echelongraph-mcp`](https://www.npmjs.com/package/echelongraph-mcp) · Official MCP
28
+ Registry: `io.echelongraph/echelongraph-mcp` · Docs: <https://echelongraph.io/pulse/mcp>
29
+ - Source: <https://github.com/echelongraph/echelongraph-mcp> · Changes:
30
+ [CHANGELOG.md](CHANGELOG.md) · Security: [SECURITY.md](SECURITY.md)
31
+
32
+ ## Quick start
33
+
34
+ The local server runs through `npx` and needs Node.js 20 or later; nothing is installed
35
+ globally. Add one of the blocks below to your client, restart it, then ask: *"Is
36
+ CVE-2023-44487 actively exploited, and how many exposed services does EchelonGraph's radar
37
+ have on record for it?"*
38
+
39
+ ### Claude Desktop
40
+
41
+ Settings → Developer → Edit Config opens `claude_desktop_config.json`
42
+ (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`; Windows:
43
+ `%APPDATA%\Claude\claude_desktop_config.json`). Add:
44
+
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "echelongraph": {
49
+ "command": "npx",
50
+ "args": ["-y", "echelongraph-mcp"]
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ### Claude Code
57
+
58
+ ```bash
59
+ claude mcp add --transport stdio echelongraph -- npx -y echelongraph-mcp
60
+ ```
61
+
62
+ Add `--scope user` before the `--` to make it available in every project, or `--scope project`
63
+ to share it through the project's `.mcp.json`.
64
+
65
+ ### Cursor
66
+
67
+ `~/.cursor/mcp.json` (every project) or `.cursor/mcp.json` (one project):
68
+
69
+ ```json
70
+ {
71
+ "mcpServers": {
72
+ "echelongraph": {
73
+ "command": "npx",
74
+ "args": ["-y", "echelongraph-mcp"]
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ ### VS Code
81
+
82
+ `.vscode/mcp.json` in the workspace (VS Code's key is `servers`, not `mcpServers`):
83
+
84
+ ```json
85
+ {
86
+ "servers": {
87
+ "echelongraph": {
88
+ "command": "npx",
89
+ "args": ["-y", "echelongraph-mcp"]
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ or from a terminal:
96
+
97
+ ```bash
98
+ code --add-mcp '{"name":"echelongraph","command":"npx","args":["-y","echelongraph-mcp"]}'
99
+ ```
100
+
101
+ ### Windsurf, Cline and other clients
102
+
103
+ Clients that read an `mcpServers` block (Windsurf's `mcp_config.json`, Cline's MCP settings,
104
+ and most others) take the same block as Cursor above.
105
+
106
+ ### Remote (no install)
107
+
108
+ The same 14 tools, four prompts and three resources are served over Streamable HTTP at:
109
+
110
+ ```text
111
+ https://mcp.echelongraph.io/mcp
112
+ ```
113
+
114
+ Keyless, no sign-in, stateless; both protocol eras. The hosted endpoint is being rolled out:
115
+ until `https://mcp.echelongraph.io/health` answers `{"status":"ok",…}`, use the `npx` setup
116
+ above.
117
+
118
+ - **claude.ai** (Free, Pro, Max, Team and Enterprise plans):
119
+ Customize → Connectors → **+ Add** → **Add custom connector**. Name it `EchelonGraph`, paste
120
+ the URL, choose **No sign in**, and add it. On Team and Enterprise an Owner adds it for the
121
+ organization first, and each member then connects it. Free plans allow one custom connector.
122
+ - **Claude Code**:
123
+
124
+ ```bash
125
+ claude mcp add --transport http echelongraph https://mcp.echelongraph.io/mcp
126
+ ```
127
+
128
+ - **Cursor** (`mcp.json`):
129
+
130
+ ```json
131
+ { "mcpServers": { "echelongraph": { "url": "https://mcp.echelongraph.io/mcp" } } }
132
+ ```
133
+
134
+ - **VS Code** (`.vscode/mcp.json`):
135
+
136
+ ```json
137
+ { "servers": { "echelongraph": { "type": "http", "url": "https://mcp.echelongraph.io/mcp" } } }
138
+ ```
139
+
140
+ - **Windsurf** (`mcp_config.json`):
141
+
142
+ ```json
143
+ { "mcpServers": { "echelongraph": { "serverUrl": "https://mcp.echelongraph.io/mcp" } } }
144
+ ```
145
+
146
+ The hosted endpoint runs this package's code (`dist/http.js`, below) against the same public
147
+ API, so its answers are the npm package's answers. What it sees and logs is under "Privacy",
148
+ and its per-client limit under "Rate limits".
149
+
16
150
  ## Tools
17
151
 
18
- | Tool | What it does |
19
- |---|---|
20
- | `cve_summary` | Counts of active CVEs by severity band, the count with no severity band from any source (`summary.none`, sent again as `summary.unscored`: CVEs not yet scored, not a rating of None), the same CVEs counted by NVD's severity label, as provenance (`summary.nvd_critical` to `summary.nvd_none`), the rejected (withdrawn) records outside the total (`summary.rejected`), and when the feed was last updated. |
21
- | `search_cves` | Search/filter CVEs (severity, min CVSS, text, sort) with EchelonGraph scores and `score_assessed`; the note names each row not yet scored. |
22
- | `get_cve` | Full record for one CVE: CVSS v3 and (when scored) v4, the EchelonGraph score and its confidence, whether EchelonGraph has scored it (`score_assessed`), EPSS, CISA-KEV status and known ransomware use, the GitHub GHSA id, references, and its published, modified and `updated_at` times. |
23
- | `cve_exposure` | Internet-exposure footprint for a CVE: exposed service count (distinct ip:port, the `exposed_hosts` field) + country/product breakdown, from the KEV-exposure radar. |
24
- | `exposure_radar` | Aggregate totals across the exposure radars: services running CISA-KEV CVEs; unauthenticated data stores and observability UIs, found through Shodan (LeakIX when Shodan query credits run low) and then confirmed by EchelonGraph's own identified check, which is not a pure read (on Redis it names its client; on ClickHouse its query lands in the server's query log); leaked credentials; shadow AI; and MCP servers found in EchelonGraph's own Certificate Transparency feed, by RFC 9728 verdict, protocol era and transport. Every number is labelled by what it counts, and a field the tool cannot label is left out and named. |
25
- | `kev_recent` | The CVEs CISA has added to its Known Exploited Vulnerabilities catalog, newest first (`kev_added_date`), from EchelonGraph's copy of the catalog, polled from CISA every 5 minutes: due date, vendor, product, known ransomware use, EchelonGraph's severity, CVSS, EPSS and `eg_kev_tier`, and `our_first_seen_kev`. Filter by date range, ransomware and vendor; page with `limit` and `next_cursor`. Dated by `last_successful_fetch_at`, our last successful fetch of CISA's feed; the filters travel as request headers, never in the URL. |
26
- | `epss_history` | How one CVE's EPSS score has changed, as EchelonGraph recorded it: one point per recorded change (`series_kind` `change_only`), never a daily series, with the value now and `series_starts_at`, when recording began; before it a missing point means not recorded, not unchanged. |
27
- | `check_affected` | Whether a product (its NVD CPE product token) or a registry package (`ecosystem` and `package`) at a given version is affected by known CVEs, from the matcher behind echelongraph.io/am-i-affected: `assessed` first (false: not evaluated, with `not_assessed_reason`, and a count of 0 then is not "not affected"), the matching CVEs with `kev_listed`, `ransomware`, `epss_score`, `effective_score` and `score_assessed`, and advisories it cannot decide counted as `undetermined_count`, never as safe. What you look up travels in request headers, never in the URL. |
28
- | `check_sbom` | A dependency list checked against EchelonGraph's advisory corpus (OSV.dev records), one verdict per component: `affected`, `not_affected`, `undetermined` or `not_assessed`, each with its `not_assessed_reason`. Pass up to 200 purls, or a CycloneDX JSON or SPDX JSON document: the purls are read from it on your machine and only they are sent, in a POST body; the document is not. A deb, apk or rpm purl without a `distro` qualifier naming its release is not assessed (`distro_release_unknown`): EchelonGraph does not guess a release. Only `not_affected` is clean. No ranking, no score. |
29
- | `cve_intel` | Weakness, public exploit code, affected packages and fixed versions for one CVE, from EchelonGraph's per-CVE enrichment: `cwes`, `exploits` (at most 10, verified first) with `exploits_total`, `exploits_capped`, `exploits_by_kind` and `exploits_by_status`, `affected_packages`, `fixed_versions` and `timeline`. `verified_status` is the label stored with each reference, not a guarantee that the exploit works. An empty `exploits` list is not evidence that no public exploit exists; a section the API could not read is named in `coverage.sections_failed`, never relayed as an empty list. |
30
- | `get_cwe` | One CWE (weakness class) and the CVEs classified under it: `name` and `description` from the MITRE CWE catalog EchelonGraph embeds, `total`, and one page of 50 `cves`, ordered as `order` states (CISA-KEV-listed first, then EchelonGraph score). A `total` of 0 says that no CVE in EchelonGraph's feed is classified under that CWE, not that none exists. |
31
- | `vendor_advisories_for_cve` | The vendor-published advisories (Microsoft MSRC, Red Hat, Cisco, Palo Alto Networks, GitHub GHSA and the other feeds EchelonGraph polls) that name one CVE, newest first, at most 20. |
32
- | `get_vendor_advisory` | One vendor advisory in full: description, severity, `cve_ids` and the subset with a CVE record here (`known_cve_ids`), `affected_products`, `remediation` and `references`. |
33
- | `search_vendor_advisories` | Search vendor advisories by text (title, description, vendor, advisory ID, products, CVE IDs), vendor, severity and whether they name a CVE. The search text is sent in a request header, never in the URL. |
152
+ Every tool is read-only (`readOnlyHint: true`) and declares a title and an `outputSchema`.
153
+ The example questions are ones a client can answer with that tool alone.
154
+
155
+ | Tool | Title | What it answers | Example question |
156
+ |---|---|---|---|
157
+ | `cve_summary` | CVE feed summary | Counts of active CVEs by severity band, the count with no severity band from any source (`summary.none`, sent again as `summary.unscored`: CVEs not yet scored, not a rating of None), the same CVEs counted by NVD's severity label, as provenance (`summary.nvd_critical` to `summary.nvd_none`), the rejected (withdrawn) records outside the total (`summary.rejected`), and when the feed was last updated. | *How many critical CVEs does the feed hold, and when was it last updated?* |
158
+ | `search_cves` | Search CVEs | Search/filter CVEs (severity, min CVSS, text, sort) with EchelonGraph scores and `score_assessed`; the note names each row not yet scored. | *Find critical CVEs that mention Tomcat with a CVSS of 9 or more.* |
159
+ | `get_cve` | CVE detail | Full record for one CVE: CVSS v3 and (when scored) v4, the EchelonGraph score and its confidence, whether EchelonGraph has scored it (`score_assessed`), EPSS, CISA-KEV status and known ransomware use, the GitHub GHSA id, references, and its published, modified and `updated_at` times. | *What are CVE-2024-3094's CVSS, EPSS and CISA-KEV status?* |
160
+ | `cve_exposure` | Internet exposure for one CVE | Internet-exposure footprint for a CVE: exposed service count (distinct ip:port, the `exposed_hosts` field) + country/product breakdown, from the KEV-exposure radar. | *How many exposed services does EchelonGraph's radar have on record for CVE-2023-44487?* |
161
+ | `exposure_radar` | Exposure radar totals | Aggregate totals across the exposure radars: services running CISA-KEV CVEs; unauthenticated data stores and observability UIs, found through Shodan (LeakIX when Shodan query credits run low) and then confirmed by EchelonGraph's own identified check, which is not a pure read (on Redis it names its client; on ClickHouse its query lands in the server's query log); leaked credentials; shadow AI; and MCP servers found in EchelonGraph's own Certificate Transparency feed, by RFC 9728 verdict, protocol era and transport. Every number is labelled by what it counts, and a field the tool cannot label is left out and named. | *How many unauthenticated data stores has EchelonGraph's radar confirmed?* |
162
+ | `kev_recent` | Recent CISA KEV additions | The CVEs CISA has added to its Known Exploited Vulnerabilities catalog, newest first (`kev_added_date`), from EchelonGraph's copy of the catalog, polled from CISA every 5 minutes: due date, vendor, product, known ransomware use, EchelonGraph's severity, CVSS, EPSS and `eg_kev_tier`, and `our_first_seen_kev`. Filter by date range, ransomware and vendor; page with `limit` and `next_cursor`. Dated by `last_successful_fetch_at`, our last successful fetch of CISA's feed; the filters travel as request headers, never in the URL. | *Which CVEs has CISA added to KEV since 1 September, and which have known ransomware use?* |
163
+ | `epss_history` | EPSS change history for one CVE | How one CVE's EPSS score has changed, as EchelonGraph recorded it: one point per recorded change (`series_kind` `change_only`), never a daily series, with the value now and `series_starts_at`, when recording began; before it a missing point means not recorded, not unchanged. | *How has CVE-2023-44487's EPSS score changed, as EchelonGraph recorded it?* |
164
+ | `check_affected` | Am I affected? (product or package at a version) | Whether a product (its NVD CPE product token) or a registry package (`ecosystem` and `package`) at a given version is affected by known CVEs, from the matcher behind echelongraph.io/am-i-affected: `assessed` first (false: not evaluated, with `not_assessed_reason`, and a count of 0 then is not "not affected"), the matching CVEs with `kev_listed`, `ransomware`, `epss_score`, `effective_score` and `score_assessed`, and advisories it cannot decide counted as `undetermined_count`, never as safe. What you look up travels in request headers, never in the URL. | *Is openssl 3.0.0 affected by known CVEs? Is lodash 4.17.15 on npm?* |
165
+ | `check_sbom` | Check an SBOM against the advisory corpus | A dependency list checked against EchelonGraph's advisory corpus (OSV.dev records), one verdict per component: `affected`, `not_affected`, `undetermined` or `not_assessed`, each with its `not_assessed_reason`. Pass up to 200 purls, or a CycloneDX JSON or SPDX JSON document: the purls are read from it on your machine and only they are sent, in a POST body; the document is not. A deb, apk or rpm purl without a `distro` qualifier naming its release is not assessed (`distro_release_unknown`): EchelonGraph does not guess a release. Only `not_affected` is clean. No ranking, no score. | *Check this CycloneDX SBOM against the advisory corpus.* |
166
+ | `cve_intel` | CVE weakness, exploits and packages | Weakness, public exploit code, affected packages and fixed versions for one CVE, from EchelonGraph's per-CVE enrichment: `cwes`, `exploits` (at most 10, verified first) with `exploits_total`, `exploits_capped`, `exploits_by_kind` and `exploits_by_status`, `affected_packages`, `fixed_versions` and `timeline`. `verified_status` is the label stored with each reference, not a guarantee that the exploit works. An empty `exploits` list is not evidence that no public exploit exists; a section the API could not read is named in `coverage.sections_failed`, never relayed as an empty list. | *Is there public exploit code for CVE-2021-44228, and which versions fix it?* |
167
+ | `get_cwe` | CWE and its CVEs | One CWE (weakness class) and the CVEs classified under it: `name` and `description` from the MITRE CWE catalog EchelonGraph embeds, `total`, and one page of 50 `cves`, ordered as `order` states (CISA-KEV-listed first, then EchelonGraph score). A `total` of 0 says that no CVE in EchelonGraph's feed is classified under that CWE, not that none exists. | *Which CVEs are classified under CWE-79, CISA-KEV-listed first?* |
168
+ | `vendor_advisories_for_cve` | Vendor advisories for one CVE | The vendor-published advisories (Microsoft MSRC, Red Hat, Cisco, Palo Alto Networks, GitHub GHSA and the other feeds EchelonGraph polls) that name one CVE, newest first, at most 20. | *Which vendor advisories name CVE-2024-3400?* |
169
+ | `get_vendor_advisory` | Vendor advisory detail | One vendor advisory in full: description, severity, `cve_ids` and the subset with a CVE record here (`known_cve_ids`), `affected_products`, `remediation` and `references`. | *Show one of those advisories in full: affected products, remediation and references.* |
170
+ | `search_vendor_advisories` | Search vendor advisories | Search vendor advisories by text (title, description, vendor, advisory ID, products, CVE IDs), vendor, severity and whether they name a CVE. The search text is sent in a request header, never in the URL. | *Search vendor advisories for FortiOS, critical severity only.* |
34
171
 
35
172
  The three vendor-advisory tools relay `vendor_published_at` (the vendor's date), `our_first_seen_at`
36
173
  (when EchelonGraph first recorded the advisory) and `withdrawn` (the vendor rescinded it; the note
@@ -321,6 +458,28 @@ keeps `kev_listed`, `ransomware`, `epss_score`, `effective_score`, `effective_se
321
458
  trace spans that record URLs do not hold what was looked up. The endpoint reads a CPE vendor
322
459
  from the URL only, so this tool takes none.
323
460
 
461
+ ## Prompts
462
+
463
+ Ready-made workflows a client can show as slash commands. Each prompt names the tools to call, in
464
+ order, and tells the model to read every result's `state`, `measured_at`, `method`, `coverage`
465
+ and `notes`: `not_assessed` is never reported as clean, a count of 0 under `assessed: false` is
466
+ never "not affected", and each figure cites its `measured_at`. A prompt makes no API request.
467
+
468
+ | Prompt | Arguments | What it asks the model to do |
469
+ |---|---|---|
470
+ | `triage_cve` | `cve_id` | `get_cve`, `cve_intel`, `vendor_advisories_for_cve`, `epss_history` and `cve_exposure` (and `kev_recent` for the due date when the CVE is KEV-listed), then a decision template: exploited, likelihood, reachable, patch, deadline, decision. |
471
+ | `kev_weekly_brief` | `days` (1 to 365, default 7) | Work out the start date from `days`, page through `kev_recent` from it, and write a brief grouped by vendor with ransomware flags and due dates. |
472
+ | `am_i_affected` | `product`, or `ecosystem` and `package`; `version` | `check_affected`, read `assessed` before `count`, with the not-assessed wording. |
473
+ | `sbom_review` | `sbom` (CycloneDX or SPDX JSON text, up to 5,000,000 characters) | Pass the document to `check_sbom`, look up each affected CVE with `get_cve`, and write a fix list ordered by CISA-KEV listing, then EPSS, then score, each key stated. |
474
+
475
+ ## Resources
476
+
477
+ | Resource | What it holds |
478
+ |---|---|
479
+ | `echelongraph://methodology` | What each envelope field and `state` value means, and how each tool measures (Markdown). |
480
+ | `echelongraph://sources` | What the API reports about its feeds when the resource is read: the NVD poller's interval and last poll (of the instance that answered, from `/api/v1/public/cves/summary`) and the CISA KEV catalog's last successful fetch and method (from `/api/v1/public/kev/recent`). It names the feeds no endpoint reports a schedule for, and states none for them (JSON). |
481
+ | `cve://{cve_id}` | One CVE as `get_cve` returns it: the structured result, envelope and record, as JSON. An ID that is not a CVE ID is refused before any request; a CVE the API has no record of is not found. |
482
+
324
483
  ## What a result means
325
484
 
326
485
  Every tool answers in one of two shapes, so a model reading the result cannot mistake an
@@ -436,7 +595,7 @@ field the API adds later is still relayed by every tool that relays the API's JS
436
595
  The server answers both eras of the Model Context Protocol on stdio:
437
596
 
438
597
  - **2026-07-28**: a client that opens with `server/discover` receives a DiscoverResult listing
439
- `2026-07-28`, the tools capability and the server instructions, and then sends each request
598
+ `2026-07-28`, the tools, prompts and resources capabilities and the server instructions, and then sends each request
440
599
  with the per-request `_meta` envelope.
441
600
  - **2025 and earlier**: a client that opens with `initialize`, as every 1.x SDK client does,
442
601
  negotiates `2025-11-25`, `2025-06-18`, `2025-03-26` or `2024-11-05`; a version the server does
@@ -452,52 +611,87 @@ Both handshakes also carry the server instructions: the data is public; what `st
452
611
  numbers are aggregate counts of ip:port services, not an internet-wide census; and that Shodan
453
612
  data is Shodan's.
454
613
 
455
- ## Install
456
-
457
- Requires Node.js 20 or later. Add it to your MCP client's config. It runs via `npx` — no global
458
- install needed.
614
+ ## Privacy: what is sent where
459
615
 
460
- ### Claude Desktop
616
+ **The npm package (stdio).** The server runs on your machine and sends HTTPS requests to one
617
+ host, `ECHELONGRAPH_API_BASE` (default `https://app.echelongraph.io`), one per API call a tool
618
+ makes (`exposure_radar` makes five; every other tool makes one). No telemetry: it sends nothing
619
+ else, to EchelonGraph or anyone. Every request carries the User-Agent
620
+ `echelongraph-mcp/<version> (+https://echelongraph.io/pulse/mcp)`, with the
621
+ `ECHELONGRAPH_MCP_UA` token ahead of it when you set one, so the API's access log can count use
622
+ per version. Like any HTTPS request, it also reaches the API from your network address.
461
623
 
462
- `claude_desktop_config.json` → `mcpServers`:
624
+ Where each tool's input travels:
463
625
 
464
- ```json
465
- {
466
- "mcpServers": {
467
- "echelongraph": {
468
- "command": "npx",
469
- "args": ["-y", "echelongraph-mcp"]
470
- }
471
- }
472
- }
473
- ```
474
-
475
- ### Cursor / Cline / Windsurf
476
-
477
- `~/.cursor/mcp.json` (or the client's MCP settings):
478
-
479
- ```json
480
- {
481
- "mcpServers": {
482
- "echelongraph": {
483
- "command": "npx",
484
- "args": ["-y", "echelongraph-mcp"]
485
- }
486
- }
487
- }
488
- ```
489
-
490
- Restart the client, then ask: *"Is CVE-2023-44487 actively exploited, and how many exposed
491
- services does EchelonGraph's radar have on record for it?"*
626
+ | Input | Where it is sent |
627
+ |---|---|
628
+ | A CVE ID (`get_cve`, `cve_exposure`, `epss_history`, `cve_intel`, `vendor_advisories_for_cve`), a CWE ID and page (`get_cwe`), a vendor and advisory ID (`get_vendor_advisory`) | The URL path. |
629
+ | `search_cves`: `search` | The `X-EG-Search` request header, never the URL. Its severity, minimum CVSS, sort and limit go in the query string. |
630
+ | `search_vendor_advisories`: `query` | The `X-EG-Advisory-Search` request header, never the URL. Its vendor, severity, CVE filter, limit and offset go in the query string. |
631
+ | `check_affected`: `product`, `version`, `ecosystem`, `package` | The `X-EG-Product`, `X-EG-Version`, `X-EG-Ecosystem` and `X-EG-Package` request headers, never the URL. |
632
+ | `kev_recent`: `since`, `until`, `ransomware`, `vendor`, `limit`, `cursor` | The `X-EG-Since`, `X-EG-Until`, `X-EG-Ransomware`, `X-EG-Vendor`, `X-EG-Limit` and `X-EG-Cursor` request headers, never the URL. |
633
+ | `check_sbom`: `purls`, or an `sbom` document | The purls, in a POST body. A CycloneDX or SPDX document is read on your machine and only the purls in it are sent; the document is not. |
634
+ | `cve_summary`, `exposure_radar` | No input. |
635
+
636
+ The API marks its answers to header-carried lookups `no-store`, so no shared cache keeps them,
637
+ and request logs and trace spans that record URLs do not hold what was looked up there.
638
+
639
+ **The hosted endpoint.** A request to `https://mcp.echelongraph.io/mcp` reaches EchelonGraph's
640
+ server first, which then calls the same API as above. Per request it logs the HTTP method and
641
+ path, the status, the duration, the body size, the JSON-RPC method and (for `tools/call`) the
642
+ tool name, the protocol version, whether an `Origin` header was sent, and the family of your
643
+ client's User-Agent. It never logs tool arguments or a query string. When a client passes the
644
+ per-client limit it logs that client's address key. It passes your client's public address to
645
+ the API, in a header the API trusts only with a token it checks, so the API's rate limit counts
646
+ you and not the hosted service.
647
+
648
+ EchelonGraph's privacy policy: <https://echelongraph.io/privacy>.
649
+
650
+ ## Rate limits
651
+
652
+ No API key, so the API limits each caller, one IPv4 address or one IPv6 /64 network as its
653
+ platform records it. Its WAF counts all your requests together in a 60-second window and
654
+ refuses one once the count passes the ceiling for its path:
655
+
656
+ | Path | Ceiling per minute | Tools |
657
+ |---|---|---|
658
+ | `/api/v1/public/cves` and every path under it | 12,000 | `cve_summary`, `search_cves`, `get_cve`, `epss_history`, `cve_intel`, `check_affected`, `check_sbom` |
659
+ | `/api/v1/public/cwes`, `/api/v1/public/vendor-advisories`, `/api/v1/public/kev/recent`, `/api/v1/public/shadow-ai-radar` | 6,000 | `get_cwe`, the three vendor-advisory tools, `kev_recent`, and one of `exposure_radar`'s five calls |
660
+ | any other `/api/v1/public` path | 600 | `cve_exposure`, and the rest of `exposure_radar`'s calls |
661
+
662
+ Because the count is shared, heavy use of one path also uses up the lower ceilings. On top of
663
+ that, 500 requests per second per caller is a burst cap on each serving instance, and
664
+ `check_sbom`'s batch route is charged per component: 1,200 components a minute per caller.
665
+ Over a limit the API answers 429 with `Retry-After`, and the tool returns a failure
666
+ (`state` `failed`, `error.status` 429), never empty data. The API's own documentation of these
667
+ limits: <https://echelongraph.io/pulse/api>.
668
+
669
+ The hosted endpoint adds its own limit before the API's: 120 MCP requests a minute per client
670
+ address, counted by each server instance, answered over it with HTTP 429, `Retry-After` and a
671
+ JSON-RPC error (code `-32029`) that names the limit and the seconds to wait. A request body over
672
+ 64 KiB is refused.
492
673
 
493
674
  ## Configuration
494
675
 
676
+ The npm package (stdio):
677
+
495
678
  | Env var | Default | Purpose |
496
679
  |---|---|---|
497
- | `ECHELONGRAPH_API_BASE` | `https://app.echelongraph.io` | Override the API base (self-host / proxy). |
680
+ | `ECHELONGRAPH_API_BASE` | `https://app.echelongraph.io` | Override the API base (self-host / proxy). A credential in the URL is masked wherever a result quotes the base. |
498
681
  | `ECHELONGRAPH_API_TIMEOUT_MS` | `15000` | Per-request timeout. A slower answer is reported as a failed lookup, not as empty data. |
499
682
  | `ECHELONGRAPH_MCP_UA` | unset | One product token (for example `my-monitor/1.0`) put ahead of this package's own User-Agent, so automated callers such as monitors are told apart from people using the server. A value that is not a single token is ignored. |
500
683
 
684
+ The HTTP entrypoint, `dist/http.js` (`npm run start:http`), serves the same tools over
685
+ Streamable HTTP on `POST /mcp`, with a liveness check on `GET /health`. It reads the three
686
+ variables above and:
687
+
688
+ | Env var | Default | Purpose |
689
+ |---|---|---|
690
+ | `PORT` | `8080` | The port it listens on. |
691
+ | `MCP_RATE_LIMIT_PER_MIN` | `120` | MCP requests a minute per client address, per instance. |
692
+ | `MCP_MAX_BODY_BYTES` | `65536` | The largest request body it reads. |
693
+ | `ECHELONGRAPH_FORWARD_TOKEN`, `ECHELONGRAPH_FORWARD_HOST`, `MCP_REQUIRE_FORWARD_TOKEN` | unset | EchelonGraph's hosted deployment only: the token with which it names each client to the API. Leave them unset when you run it yourself; the API then counts every request as coming from your server. |
694
+
501
695
  ## Develop
502
696
 
503
697
  ```bash
@@ -515,6 +709,19 @@ counted as external MCP adoption. `npm test` never leaves the machine.
515
709
  set `ECHELONGRAPH_MCP_BIN` to that package's `echelongraph-mcp` bin: the tests then run the bin
516
710
  directly, as `npx` does, and read that package's own files.
517
711
 
712
+ ## Versioning
713
+
714
+ Semantic versioning: a major version changes protocol behaviour or drops a Node.js version, a
715
+ minor version adds tools or fields, a patch fixes wording or behaviour without changing a tool's
716
+ shape. Every release is published to npm with a provenance attestation by the public repo's
717
+ release workflow, from the commit tagged `v<version>`. Changes per version:
718
+ [CHANGELOG.md](CHANGELOG.md).
719
+
720
+ ## Security
721
+
722
+ Report a vulnerability privately to **support@echelongraph.io**, not in a public issue. The
723
+ policy, the supported versions and the scope are in [SECURITY.md](SECURITY.md).
724
+
518
725
  ## License
519
726
 
520
727
  The code is MIT-licensed.
package/dist/index.js CHANGED
@@ -54,6 +54,8 @@ import { registerCveIntel } from "./tools/cve_intel.js";
54
54
  import { registerGetCwe } from "./tools/get_cwe.js";
55
55
  import { isHttpEntrypoint, upstreamHeaders } from "./runtime.js";
56
56
  import { registerVendorAdvisoryTools } from "./tools/vendor_advisories.js";
57
+ import { registerPrompts } from "./prompts.js";
58
+ import { registerResources } from "./resources.js";
57
59
  // The package actually running, read from the package.json that ships beside dist/. The MCP
58
60
  // handshake (serverInfo) and the User-Agent both carry its name and version, so the API's
59
61
  // access log can count adoption per version without a code change per release.
@@ -519,9 +521,10 @@ async function cveSummary() {
519
521
  async function searchCVEs(a) {
520
522
  const tool = "search_cves";
521
523
  try {
524
+ // #1983: the free-text search travels percent-encoded in X-EG-Search, never in the URL. Cloud
525
+ // Run keeps every request URL in its trace spans for 30 days and no request header; core-backend
526
+ // (cve/handler.go listSearchTerm) reads the header first and decodes it with url.PathUnescape.
522
527
  const q = new URLSearchParams();
523
- if (a.search)
524
- q.set("search", a.search);
525
528
  if (a.severity)
526
529
  q.set("severity", a.severity);
527
530
  if (a.min_cvss !== undefined)
@@ -529,7 +532,7 @@ async function searchCVEs(a) {
529
532
  if (a.sort)
530
533
  q.set("sort", a.sort);
531
534
  q.set("limit", String(a.limit ?? 20));
532
- const r = await api(`/api/v1/public/cves?${q.toString()}`);
535
+ const r = await api(`/api/v1/public/cves?${q.toString()}`, a.search ? { headers: { "X-EG-Search": encodeURIComponent(a.search) } } : undefined);
533
536
  if (!r.ok)
534
537
  return failed(tool, r);
535
538
  const head = okHead(tool, r.status);
@@ -1962,7 +1965,7 @@ const TOOL_KIT = { api, succeeded, failed, badInput, crashed, checked, okHead, e
1962
1965
  // per request by the hosted HTTP entrypoint (http.ts, #2316): one tool module for both.
1963
1966
  // tools/list answers in registration order, which is the order below.
1964
1967
  export function createServer() {
1965
- const server = new McpServer({ name: NAME, version: VERSION }, { instructions: INSTRUCTIONS, capabilities: { tools: { listChanged: false } } });
1968
+ const server = new McpServer({ name: NAME, version: VERSION }, { instructions: INSTRUCTIONS, capabilities: { tools: { listChanged: false }, prompts: { listChanged: false }, resources: { listChanged: false } } });
1966
1969
  server.registerTool("cve_summary", {
1967
1970
  title: "CVE feed summary",
1968
1971
  description: `Summary of EchelonGraph's CVE Pulse feed: summary.total active CVEs, their counts by severity band (summary.critical, summary.high, summary.medium, summary.low), the count with no band (summary.none), and summary.last_updated, the newest modification time among those records. ${SUMMARY_NONE_DESCRIPTION} ${SUMMARY_NVD_DESCRIPTION} ${SUMMARY_REJECTED_DESCRIPTION} The feed is polled from its sources on a schedule, so this is the state as of that update. ${FEED_ENVELOPE}`,
@@ -2012,6 +2015,10 @@ export function createServer() {
2012
2015
  registerGetCwe(server);
2013
2016
  // #2719: vendor_advisories_for_cve, get_vendor_advisory, search_vendor_advisories.
2014
2017
  registerVendorAdvisoryTools(server, { api, failed, badInput, crashed, succeeded, checked, okHead, envelopeSchema, annotations: ANNOTATIONS });
2018
+ // #2722: prompts and resources, in prompts.ts and resources.ts. Both lists are fixed per
2019
+ // connection, so listChanged is false above.
2020
+ registerPrompts(server, { cveId: CVE_ID });
2021
+ registerResources(server, { api, cveId: CVE_ID, shownBase: SHOWN_BASE, getCve: async (id) => checked("get_cve", GET_CVE_OUTPUT, await getCVE(id)) });
2015
2022
  return server;
2016
2023
  }
2017
2024
  // The package identity and API settings, for the hosted HTTP entrypoint's health answer and
@@ -0,0 +1,137 @@
1
+ import * as z from "zod";
2
+ import { MAX_COMPONENTS, MAX_SBOM_CHARS } from "./tools/check_sbom.js";
3
+ export const KEV_BRIEF_DEFAULT_DAYS = 7;
4
+ export const KEV_BRIEF_MAX_DAYS = 365;
5
+ // The envelope rules every prompt carries, word for word.
6
+ export const ENVELOPE_RULES = [
7
+ "Before you use any figure, read that result's evidence envelope: its structuredContent (where your client shows text blocks alone, the last text block) carries state, measured_at, method, coverage, freshness and notes.",
8
+ "state is measured, not_assessed, failed or invalid_input.",
9
+ "Report not_assessed as not assessed: never as clean, safe, unaffected or none found.",
10
+ 'A count of 0 in a result whose coverage says assessed: false is not a finding of "not affected": say the lookup did not evaluate it, and give its not_assessed_reason.',
11
+ "failed and invalid_input mean nothing was looked up: say the lookup did not complete, never zero, none found or unexposed.",
12
+ "Cite the measured_at date behind each figure you report, and say so where measured_at is null.",
13
+ "Quote the notes that qualify a figure, and keep each field's meaning as the result's notes give it.",
14
+ "Do not add facts the results do not carry: where a result says nothing about a point, write that it does not say.",
15
+ ].join(" ");
16
+ const text = (t) => ({ messages: [{ role: "user", content: { type: "text", text: t } }] });
17
+ export function triageCveText(cve) {
18
+ return [
19
+ `Triage ${cve} using the EchelonGraph MCP tools. Call them in this order, each with cve_id ${cve}:`,
20
+ "1. get_cve: the record. Read score_assessed before echelongraph_score (false: not yet scored, which is not a score of 0); read severity, cvss_v3_score, epss_score and epss_percentile, and the CISA-KEV fields kev_listed, kev_added_date and kev_ransomware.",
21
+ "2. cve_intel: weaknesses (cwes), public exploit code (exploits and exploits_total; verified_status is the label stored with each reference, not a guarantee that it works, and an empty list is not evidence that no exploit exists), affected_packages and fixed_versions.",
22
+ "3. vendor_advisories_for_cve: the vendor advisories that name it. Call get_vendor_advisory for one whose remediation you need in full.",
23
+ "4. epss_history: how its EPSS score changed, one point per recorded change (series_kind change_only): before series_starts_at a missing point means not recorded, not unchanged.",
24
+ "5. cve_exposure: internet exposure on record. exposure_state says what the count is; exposed_hosts counts distinct ip:port services, not machines; the result is not_assessed with measured_at null, so present any count as undated and on record, and exposure_state not_assessed as not assessed.",
25
+ `6. If get_cve says kev_listed is true: kev_recent with since and until both set to its kev_added_date, to read kev_due_date from the row for ${cve}.`,
26
+ ENVELOPE_RULES,
27
+ "Then answer with this template, one line each, naming the tool and the measured_at behind every line:",
28
+ "- Exploited? CISA-KEV listing and its date, known ransomware use, and public exploit code on record.",
29
+ "- Likelihood: EPSS score and percentile, and how it has moved.",
30
+ "- Reachable? exposure_state and the count on record, or not assessed.",
31
+ '- Patch: fixed versions and vendor advisories; where the results hold none, write "none on record", which is not a finding that no fix exists.',
32
+ "- Deadline: kev_due_date when the CVE is KEV-listed (CISA's due date for US federal civilian agencies); otherwise write that the results give no CISA deadline.",
33
+ "- Decision: patch now, schedule, or monitor, with a one-line reason drawn from the lines above.",
34
+ ].join("\n");
35
+ }
36
+ export function kevWeeklyBriefText(days) {
37
+ return [
38
+ `Write a brief of the CVEs CISA added to its Known Exploited Vulnerabilities (KEV) catalog in the last ${days} days, using the EchelonGraph MCP tools.`,
39
+ `1. Compute since: today's date in UTC minus ${days} days, as YYYY-MM-DD. State the since date you used, and today's date.`,
40
+ "2. Call kev_recent with since set to that date and limit 200. While its coverage.has_more is true, call kev_recent again with the same since and cursor set to the previous answer's next_cursor.",
41
+ ENVELOPE_RULES,
42
+ "Then write the brief:",
43
+ "- Header: the date range, the number of CVEs added (total), and measured_at, EchelonGraph's last successful fetch of CISA's feed, with catalog_version.",
44
+ "- One section per kev_vendor, vendors in alphabetical order. Under each, one line per CVE: cve_id, kev_product, kev_vuln_name, kev_added_date, kev_due_date, and RANSOMWARE where kev_ransomware is true.",
45
+ "- A closing section listing again the CVEs with kev_ransomware true, and every kev_due_date in the range, earliest first.",
46
+ "- If total is 0, write that no CVE was added in that range in the catalog copy fetched at measured_at. If kev_recent failed, write that the catalog could not be read: never that nothing was added.",
47
+ ].join("\n");
48
+ }
49
+ export function amIAffectedText(a) {
50
+ const subject = a.product ? `the product ${JSON.stringify(a.product)} (an NVD CPE product token)` : `the package ${JSON.stringify(a.package)} in the ${JSON.stringify(a.ecosystem)} ecosystem`;
51
+ const args = a.product ? { product: a.product, version: a.version } : { ecosystem: a.ecosystem, package: a.package, version: a.version };
52
+ return [
53
+ `Check whether ${subject} at version ${JSON.stringify(a.version)} is affected by known CVEs, using the EchelonGraph MCP tools.`,
54
+ `1. Call check_affected with these arguments: ${JSON.stringify(args)}.`,
55
+ ENVELOPE_RULES,
56
+ "Read coverage.assessed before count, and answer with the case that applies:",
57
+ "- assessed true and count above 0: affected. List each match's cve_id with kev_listed, ransomware, epss_score and effective_score (score_assessed false: not yet scored, not a score of 0), CISA-KEV-listed CVEs first.",
58
+ "- assessed true and count 0: no matching CVE in EchelonGraph's data at this version. Give undetermined_count too: an advisory that could not be decided at this version is not safe.",
59
+ "- assessed false: NOT ASSESSED. Write that the lookup did not evaluate this version, give not_assessed_reason, and never report it as not affected or clean.",
60
+ "- failed or invalid_input: the check did not run. Say why, from the result.",
61
+ "Where capped, candidates_capped or degraded is true, say the list may be incomplete and why.",
62
+ ].join("\n");
63
+ }
64
+ export function sbomReviewText(sbom) {
65
+ return [
66
+ "Review the SBOM below for known vulnerabilities, using the EchelonGraph MCP tools.",
67
+ `1. Call check_sbom with sbom set to the document below, as given. It reads the document's purls and checks up to ${MAX_COMPONENTS} distinct purls per call; a document with more is refused, not truncated. If it is refused, say why from the result and do not guess at the components.`,
68
+ "2. For each distinct cve_id on a component whose verdict is affected, call get_cve, to read kev_listed, epss_score, score_assessed, echelongraph_score and cvss_v3_score. If there are more than 30 distinct CVEs, do this for 30 of them and list the rest as not looked up, unranked.",
69
+ ENVELOPE_RULES,
70
+ "Read each component's verdict: not_affected is the one clean verdict; undetermined and not_assessed are not clean, and not_assessed_reason says why (for example distro_release_unknown: a deb, apk or rpm purl without a distro qualifier, which EchelonGraph does not guess).",
71
+ "Then write a prioritised fix list of the affected components. State the ordering keys, in this order: 1. CISA-KEV listed (kev_listed true) first; 2. then EPSS score, highest first; 3. then echelongraph_score where score_assessed is true, else cvss_v3_score, highest first. A CVE with no value for a key sorts after those with one, and its line says which key was missing.",
72
+ "For each line give the component (purl), its cve_ids, the key values used, and fixed versions where a result gives them. After the list, give data.summary's counts, and list the undetermined and not_assessed components with their reasons as not checked, never as clean.",
73
+ "",
74
+ "SBOM:",
75
+ "```json",
76
+ sbom,
77
+ "```",
78
+ ].join("\n");
79
+ }
80
+ export function registerPrompts(server, kit) {
81
+ server.registerPrompt("triage_cve", {
82
+ title: "Triage a CVE",
83
+ description: "Triage one CVE: get_cve, cve_intel, vendor_advisories_for_cve, epss_history and cve_exposure (and kev_recent for the due date when the CVE is KEV-listed), then a decision template: exploited, likelihood, reachable, patch, deadline. Each line cites its tool and measured_at, and not_assessed is never reported as clean.",
84
+ argsSchema: z.object({
85
+ cve_id: z
86
+ .string()
87
+ .trim()
88
+ .regex(kit.cveId, "cve_id must be a CVE ID such as CVE-2024-3400")
89
+ .describe("a CVE ID, e.g. CVE-2024-3400"),
90
+ }),
91
+ }, ({ cve_id }) => text(triageCveText(cve_id.toUpperCase())));
92
+ server.registerPrompt("kev_weekly_brief", {
93
+ title: "CISA KEV brief",
94
+ description: "A brief of the CVEs CISA added to its KEV catalog in the last N days (default 7): kev_recent from a since date the model computes from days, grouped by vendor, with ransomware flags and due dates, dated by EchelonGraph's last successful fetch of CISA's feed.",
95
+ // A client may send no arguments at all for a prompt whose arguments are all optional.
96
+ argsSchema: z.preprocess((v) => v ?? {}, z.object({
97
+ days: z
98
+ .string()
99
+ .trim()
100
+ .regex(/^\d{1,3}$/, `days must be a whole number from 1 to ${KEV_BRIEF_MAX_DAYS}`)
101
+ .refine((v) => Number(v) >= 1 && Number(v) <= KEV_BRIEF_MAX_DAYS, `days must be a whole number from 1 to ${KEV_BRIEF_MAX_DAYS}`)
102
+ .optional()
103
+ .describe(`how many days back to cover, 1 to ${KEV_BRIEF_MAX_DAYS} (default ${KEV_BRIEF_DEFAULT_DAYS})`),
104
+ })),
105
+ }, ({ days }) => text(kevWeeklyBriefText(days === undefined || days === "" ? KEV_BRIEF_DEFAULT_DAYS : Number(days))));
106
+ server.registerPrompt("am_i_affected", {
107
+ title: "Am I affected?",
108
+ description: "Whether a product (its NVD CPE product token) or a registry package (ecosystem and package) at a version is affected by known CVEs: check_affected, read assessed before count, with the not-assessed wording: a count of 0 under assessed false is never reported as not affected.",
109
+ argsSchema: z
110
+ .object({
111
+ product: z.string().trim().min(1).optional().describe("CPE path: the NVD CPE product token, such as openssl or nginx. Leave out for the registry path."),
112
+ ecosystem: z.string().trim().min(1).optional().describe("Registry path: the package's ecosystem, such as npm, PyPI or Maven. Give with package."),
113
+ package: z.string().trim().min(1).optional().describe("Registry path: the package name, such as lodash. Give with ecosystem."),
114
+ version: z.string().trim().min(1).describe("The version to check, such as 3.0.0."),
115
+ })
116
+ .superRefine((a, ctx) => {
117
+ const registry = a.ecosystem !== undefined || a.package !== undefined;
118
+ if (a.product !== undefined && registry) {
119
+ ctx.addIssue({ code: "custom", message: "give product, or ecosystem and package, not both" });
120
+ }
121
+ else if (a.product === undefined && (a.ecosystem === undefined || a.package === undefined)) {
122
+ ctx.addIssue({ code: "custom", message: "give product, or both ecosystem and package" });
123
+ }
124
+ }),
125
+ }, (a) => text(amIAffectedText(a)));
126
+ server.registerPrompt("sbom_review", {
127
+ title: "SBOM review",
128
+ description: `Review an SBOM (CycloneDX JSON or SPDX JSON text, up to ${MAX_SBOM_CHARS} characters): it is passed to check_sbom, which checks up to ${MAX_COMPONENTS} distinct purls per call, then get_cve for each affected CVE, and a prioritised fix list ordered by CISA-KEV listing, then EPSS, then score, each key stated. undetermined and not_assessed components are listed as not checked, never as clean.`,
129
+ argsSchema: z.object({
130
+ sbom: z
131
+ .string()
132
+ .min(1, "sbom is required")
133
+ .max(MAX_SBOM_CHARS, `sbom is longer than ${MAX_SBOM_CHARS} characters, check_sbom's cap`)
134
+ .describe(`a CycloneDX JSON or SPDX JSON document, as JSON text, up to ${MAX_SBOM_CHARS} characters; it is passed to check_sbom`),
135
+ }),
136
+ }, ({ sbom }) => text(sbomReviewText(sbom)));
137
+ }
@@ -0,0 +1,153 @@
1
+ // MCP resources (#2722): context a client can attach.
2
+ //
3
+ // echelongraph://methodology static text: what each evidence-envelope field means, the state
4
+ // values, and how each tool measures (from the README's envelope
5
+ // docs). No API request.
6
+ // echelongraph://sources the feeds and what the API itself reports about their polling,
7
+ // read from the API at request time and never hard-coded: the CVE
8
+ // summary's poller block (/api/v1/public/cves/summary) and the KEV
9
+ // answer's catalog block and method (/api/v1/public/kev/recent).
10
+ // What no endpoint reports is named as not reported.
11
+ // cve://{cve_id} get_cve's structured result for one CVE, as JSON: the envelope
12
+ // (state, measured_at, method, notes) with data, the API's record.
13
+ //
14
+ // A resource read either answers or is a JSON-RPC error; it never answers a failure as data that
15
+ // reads like a record. sources is the exception by design: each feed it reads carries its own
16
+ // state, and a feed that could not be read says so (state failed), never an empty block.
17
+ import { ProtocolError, ProtocolErrorCode, ResourceNotFoundError, ResourceTemplate } from "@modelcontextprotocol/server";
18
+ export const METHODOLOGY_URI = "echelongraph://methodology";
19
+ export const SOURCES_URI = "echelongraph://sources";
20
+ export const CVE_TEMPLATE = "cve://{cve_id}";
21
+ export const METHODOLOGY = `# How EchelonGraph MCP results are measured
22
+
23
+ Every tool result carries an evidence envelope in its structuredContent, and repeats it in its last text block (less what an earlier text block already carries verbatim), so a client that passes text blocks alone still sees it.
24
+
25
+ ## Envelope fields
26
+
27
+ | Field | What it holds |
28
+ |---|---|
29
+ | state | measured, not_assessed, failed or invalid_input (below). |
30
+ | measured_at | When the underlying observation was made, as the API states it. null when the answer does not say or holds no observation; never the Go zero time. |
31
+ | method | How the numbers were produced. null on a failure. |
32
+ | coverage | What the answer covers, where the tool can say: for example in_scope for cve_exposure, the list's account of its own count for search_cves, assessed and not_assessed_reason first for check_affected. |
33
+ | freshness | The producing radar's or fetcher's last completed check, where the API serves one; null where it serves none. |
34
+ | notes | The caveats, one sentence each. |
35
+ | data | On a success: the API's JSON (exposure_radar and cve_intel relay a labelled selection). |
36
+ | error | On a failure: kind, path, status and message. |
37
+
38
+ ## state
39
+
40
+ | state | What it means |
41
+ |---|---|
42
+ | measured | A measurement of what was asked. An exposure count is measured when the answer says when what it counts was observed and how it was produced; no exposure answer says when today, so neither exposure tool answers measured. |
43
+ | not_assessed | The answer holds no dated measurement of what was asked, so no count in it is presented as one. It can still relay a count, as what the source holds on record, undated; the notes say what each count is. Never clean. |
44
+ | failed | The lookup did not complete: unreachable host, non-2xx, timeout, or a body that is not a JSON object. Not a finding, never zero. |
45
+ | invalid_input | The input was refused, by this server or by the API, so nothing was looked up. Not a finding. |
46
+
47
+ A count of 0 is a finding of none when the answer measured it. Under assessed: false, or state not_assessed, it is not: report it as not assessed.
48
+
49
+ ## How each tool measures
50
+
51
+ - cve_summary: measured; measured_at is summary.last_updated, the newest modification time among the active CVE records it counts. freshness is null: the feed serves no poll-completion time for the feed as a whole.
52
+ - search_cves: measured, measured_at null (each record carries its own times); coverage repeats total, total_counted, total_is_lower_bound, search_relaxed, limit and offset.
53
+ - get_cve: measured; measured_at is the record's updated_at, when EchelonGraph last wrote it. score_assessed false means not yet scored, not a score of 0.
54
+ - cve_exposure: not_assessed with measured_at null, every answer: its last_seen is when EchelonGraph last wrote or refreshed a service's row, not an observation time. exposure_state (exposed, measured_zero, not_assessed, tracking_unknown) says what the count is; exposed_hosts counts distinct ip:port services from Shodan data, not machines, and is not an internet-wide census.
55
+ - exposure_radar: not_assessed with measured_at null: no radar's answer gives one time at which what it counts was observed. freshness gives each radar's last_run_at where the API serves one.
56
+ - kev_recent: measured; measured_at is EchelonGraph's last successful fetch of CISA's KEV feed; freshness carries that fetch time, catalog_version and date_released.
57
+ - epss_history: measured_at is current.epss_updated_at, when EchelonGraph last wrote a changed EPSS value; the series is change-only, so a missing point is not recorded, not unchanged.
58
+ - check_affected: measured when the answer says assessed true; not_assessed when it says false or does not say. measured_at and freshness are null.
59
+ - check_sbom: measured when at least one component got a verdict, else not_assessed; measured_at null. not_affected is the one clean verdict; undetermined and not_assessed are not clean.
60
+ - cve_intel: measured_at null: each row carries its own first_seen_at or enriched_at. An empty exploits list is not evidence that no public exploit exists.
61
+ - get_cwe: measured_at null: each row carries its own published date. A total of 0 says no CVE in EchelonGraph's feed is classified under that CWE.
62
+ - vendor_advisories_for_cve, search_vendor_advisories: measured_at null (each row carries vendor_published_at and our_first_seen_at). get_vendor_advisory: measured_at is our_first_seen_at, when EchelonGraph first recorded the advisory.
63
+
64
+ Every figure is as fresh as the schedule that refreshes it. The resource ${SOURCES_URI} reports what the API itself says about its feeds' polling.
65
+ `;
66
+ const SUMMARY_PATH = "/api/v1/public/cves/summary";
67
+ const KEV_PATH = "/api/v1/public/kev/recent";
68
+ const obj = (v) => (v !== null && typeof v === "object" && !Array.isArray(v) ? v : undefined);
69
+ const pick = (o, keys) => Object.fromEntries(keys.filter((k) => o !== undefined && o[k] !== undefined).map((k) => [k, o[k]]));
70
+ const failure = (r) => ({ state: "failed", error: { kind: r.kind, path: r.path, status: r.status ?? null, message: r.detail } });
71
+ // What /api/v1/public/cves/summary reports about the NVD poller: its interval and last poll, of
72
+ // the server instance that answered (core-backend cve/poller.go Stats, in memory).
73
+ const POLLER_FIELDS = ["interval", "last_poll_at", "poll_count", "poll_errors"];
74
+ const CATALOG_FIELDS = ["source", "feed_url", "last_successful_fetch_at", "catalog_version", "date_released", "catalog_count"];
75
+ export async function readSources(kit) {
76
+ const [summary, kev] = await Promise.all([kit.api(SUMMARY_PATH), kit.api(KEV_PATH, { headers: { "X-EG-Limit": "1" } })]);
77
+ const nvd = { feed: "NVD CVE records (EchelonGraph's CVE feed poller)", endpoint: SUMMARY_PATH };
78
+ if (!summary.ok)
79
+ Object.assign(nvd, failure(summary));
80
+ else {
81
+ const poller = obj(obj(summary.data)?.poller);
82
+ if (poller === undefined) {
83
+ Object.assign(nvd, { state: "not_reported", reported: null, notes: ["The answer carries no poller block, so it reports neither a poll interval nor a last poll time."] });
84
+ }
85
+ else {
86
+ Object.assign(nvd, {
87
+ state: "reported",
88
+ reported: pick(poller, POLLER_FIELDS),
89
+ notes: [
90
+ "poller is the NVD poller of the server instance that answered, held in its memory: interval is its configured interval (each cycle adds a start-up jitter), and last_poll_at is that instance's last poll, not a fleet-wide time.",
91
+ ],
92
+ });
93
+ }
94
+ }
95
+ const cisa = { feed: "CISA Known Exploited Vulnerabilities catalog", endpoint: KEV_PATH };
96
+ if (!kev.ok)
97
+ Object.assign(cisa, failure(kev));
98
+ else {
99
+ const d = obj(kev.data);
100
+ const catalog = obj(d?.catalog);
101
+ const method = typeof d?.method === "string" ? d.method : null;
102
+ Object.assign(cisa, {
103
+ state: catalog === undefined && method === null ? "not_reported" : "reported",
104
+ reported: { catalog: catalog === undefined ? null : pick(catalog, CATALOG_FIELDS), method },
105
+ notes: [
106
+ "method is the API's own sentence about how it polls CISA's feed, quoted as sent; catalog.last_successful_fetch_at is when CISA last answered that poll.",
107
+ ],
108
+ });
109
+ }
110
+ return {
111
+ uri: SOURCES_URI,
112
+ api_base: kit.shownBase,
113
+ read_at: new Date().toISOString(),
114
+ sources: [nvd, cisa],
115
+ not_reported: [
116
+ "No public endpoint this server reads reports a poll cadence or a last poll time for the MITRE-CNA pre-NVD records, EPSS, GitHub GHSA, the OSV.dev advisory records, the vendor-advisory feeds, or the exposure radars' sources; this resource states none for them.",
117
+ "exposure_radar's freshness gives each radar's last_run_at, its last completed check, where the API serves one.",
118
+ ],
119
+ };
120
+ }
121
+ const json = (uri, v) => ({ contents: [{ uri, mimeType: "application/json", text: JSON.stringify(v, null, 2) }] });
122
+ export function registerResources(server, kit) {
123
+ server.registerResource("methodology", METHODOLOGY_URI, {
124
+ title: "How results are measured",
125
+ description: "What each evidence-envelope field (state, measured_at, method, coverage, freshness, notes) means, the state values, and how each tool measures.",
126
+ mimeType: "text/markdown",
127
+ }, async (uri) => ({ contents: [{ uri: uri.href, mimeType: "text/markdown", text: METHODOLOGY }] }));
128
+ server.registerResource("sources", SOURCES_URI, {
129
+ title: "Feed sources and polling",
130
+ description: "The feeds and what the API reports about their polling, read from the API when the resource is read: the NVD poller's interval and last poll (of the instance that answered) and the CISA KEV catalog's last successful fetch and method. What no endpoint reports is named as not reported.",
131
+ mimeType: "application/json",
132
+ }, async (uri) => json(uri.href, await readSources(kit)));
133
+ server.registerResource("cve", new ResourceTemplate(CVE_TEMPLATE, { list: undefined }), {
134
+ title: "CVE record",
135
+ description: "One CVE's record as get_cve returns it, as JSON: the evidence envelope (state, measured_at, method, notes) with data, the API's record. cve_id is a CVE ID such as CVE-2024-3400.",
136
+ mimeType: "application/json",
137
+ }, async (uri, vars) => {
138
+ const raw = Array.isArray(vars.cve_id) ? vars.cve_id[0] : vars.cve_id;
139
+ const id = decodeURIComponent(String(raw ?? "")).trim();
140
+ if (!kit.cveId.test(id)) {
141
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, `cve:// takes a CVE ID such as CVE-2024-3400 (${id.length} characters given)`, { uri: uri.href, reason: "invalid_cve_id" });
142
+ }
143
+ const r = await kit.getCve(id.toUpperCase());
144
+ if (r.isError) {
145
+ const msg = r.content[0]?.text ?? "get_cve failed";
146
+ const err = obj(r.structuredContent.error);
147
+ if (err?.status === 404)
148
+ throw new ResourceNotFoundError(uri.href);
149
+ throw new ProtocolError(ProtocolErrorCode.InternalError, msg, { uri: uri.href, state: r.structuredContent.state ?? null, error: err ?? null });
150
+ }
151
+ return json(uri.href, r.structuredContent);
152
+ });
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echelongraph-mcp",
3
- "version": "2.4.0",
3
+ "version": "2.5.1",
4
4
  "mcpName": "io.echelongraph/echelongraph-mcp",
5
5
  "description": "EchelonGraph CVE and internet-exposure data as an MCP server: CVEs with EchelonGraph multi-source scores, CISA-KEV and EPSS, plus a per-CVE internet-exposure footprint derived from Shodan data. Shodan data is owned by Shodan, which holds its copyright (© Shodan). Free and keyless, for Claude, Cursor and any MCP client.",
6
6
  "type": "module",
@@ -10,6 +10,7 @@
10
10
  "files": [
11
11
  "dist",
12
12
  "README.md",
13
+ "CHANGELOG.md",
13
14
  "server.json"
14
15
  ],
15
16
  "scripts": {
package/server.json CHANGED
@@ -2,13 +2,22 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.echelongraph/echelongraph-mcp",
4
4
  "title": "EchelonGraph CVE & Exposure",
5
- "description": "CVEs (NVD, CISA-KEV, EPSS, GHSA) and per-CVE exposure from Shodan data (© Shodan). Free, keyless.",
5
+ "description": "CVE, KEV, EPSS, SBOM and advisory lookups; per-CVE exposure from Shodan data (© Shodan). Keyless.",
6
6
  "websiteUrl": "https://echelongraph.io/pulse/mcp",
7
7
  "repository": {
8
8
  "url": "https://github.com/echelongraph/echelongraph-mcp",
9
9
  "source": "github"
10
10
  },
11
- "version": "2.4.0",
11
+ "icons": [
12
+ {
13
+ "src": "https://echelongraph.io/logo-mark.png",
14
+ "mimeType": "image/png",
15
+ "sizes": [
16
+ "512x512"
17
+ ]
18
+ }
19
+ ],
20
+ "version": "2.5.1",
12
21
  "remotes": [
13
22
  {
14
23
  "type": "streamable-http",
@@ -19,7 +28,7 @@
19
28
  {
20
29
  "registryType": "npm",
21
30
  "identifier": "echelongraph-mcp",
22
- "version": "2.4.0",
31
+ "version": "2.5.1",
23
32
  "transport": {
24
33
  "type": "stdio"
25
34
  },