echelongraph-mcp 2.6.3 → 2.6.5

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 CHANGED
@@ -6,6 +6,195 @@ with a provenance attestation by the release workflow of
6
6
  [github.com/echelongraph/echelongraph-mcp](https://github.com/echelongraph/echelongraph-mcp),
7
7
  from the commit tagged `v<version>`.
8
8
 
9
+ ## 2.6.5 — 2026-10-05
10
+
11
+ 2.6.4 was tagged and never published: its Release run failed `test/http-memory.test.mjs` on Node 24,
12
+ where `--max-old-space-size=128` alone allowed a 320 MiB heap in a 256 MiB instance. 2.6.5 is 2.6.4
13
+ plus the fix below; the `v2.6.4` tag stays, unpublished, because a tag is never moved.
14
+
15
+ - Hosted endpoint only: `Dockerfile.http` also passes `--max-semi-space-size=16`, so V8's heap limit is
16
+ 176 MiB on Node 22 (unchanged: the production image) and on Node 24 (was 320 MiB). The memory cap of
17
+ #2773 now holds whichever Node the image uses.
18
+
19
+ - README: the header links the directories that list this server, Smithery and Glama (#2723). Smithery's
20
+ verification looks for that link; `test/listings.test.mjs` pins it.
21
+
22
+ - Hosted endpoint only: an instance serves at most 64 requests at once (`MCP_MAX_IN_FLIGHT`); the
23
+ next, its body read first (so a slow upload holds no place), waits up to 10 seconds
24
+ (`MCP_ADMISSION_WAIT_MS`) for one to finish, and is then answered HTTP 503 with `Retry-After` and
25
+ JSON-RPC error `-32030`. A 2026-07-28 `subscriptions/listen` stream is not counted: it stays open
26
+ until its client leaves, carrying only keepalives (the server announces no list changes), so 64
27
+ idle ones would leave every other request waiting 10 s for a 503; the SDK refuses a stream past
28
+ 1,024 open per process. The image runs Node with V8's heap capped at 128 MiB, half the
29
+ instance's memory. Measured in a 256 MiB cgroup on a 16 GB host: two maximum-size SBOM documents
30
+ beside 125 or more other requests got the server killed with every request on it, and holding
31
+ that took both bounds. Node sized V8's heap from the host's memory, so garbage piled up until
32
+ the kernel killed the process (whether Cloud Run's instance reports more than its 256 MiB is not
33
+ verified), and 250 requests live at once exhausted even the capped heap. With both, 250 sent as
34
+ they come are all served (the last after waiting about 9 s), peaking at 82-84% of the memory,
35
+ and 250 at once are 64 served and 186 answered 503 after 10 s, peaking at 75-77%. The `mcp_request` line gains `in_flight` and
36
+ `queued_ms`, the start-up line `heap_limit_mb` and `memory_limit_mb`, and a process started
37
+ without the cap logs `mcp_remote_heap_unbounded`, where Node can read the container's limit
38
+ (`memory_limit_mb` not null) (#2773).
39
+ - `check_sbom` stops when its call is cancelled: no further batch is sent, the batch in flight is
40
+ cut off, and a `Retry-After` wait ends. Over stdio that is the client's
41
+ `notifications/cancelled`, which before left the call sending its batches to the end. On the
42
+ hosted endpoint it is the client going away (its own timeout, a closed tab): every API call of
43
+ that request is cancelled, a `check_sbom` call holds its large-body slot until it has stopped,
44
+ not only until its client has gone, and the `mcp_request` line says `status` 499 with
45
+ `client_closed` true. Before, an abandoned call ran its batches to the end, spending the
46
+ client's API budget, and was logged as 200 (#2775).
47
+ - Repository only, not in the package: the production synthetic's hosted leg sends `check_sbom`
48
+ its 201 purls as a CycloneDX document, a ~138,000-byte request body, twice the endpoint's
49
+ 64 KiB general cap, and fails the probe unless the answer is `measured` from the document; a
50
+ return of the 413 that #2747 removed now fails it (`rpc_error`, `-32600`). The synthetic's alert
51
+ runbook says how to read that failure, and a memory alert on the hosted endpoint is written
52
+ beside its uptime check (#2774, #2773).
53
+ - The prompts ask only for what their tools' first text block keeps, at their largest input. In
54
+ 2.6.3 each of the four asked for something that text leaves out of a large answer, or that the
55
+ call it made did not return, so a client that passes only `content` to the model never showed
56
+ it (#2799):
57
+ - `kev_weekly_brief` reads `kev_recent` 50 rows a page, not 200. Each line of the brief names
58
+ `kev_vuln_name` and `kev_due_date`. Read through the hosted endpoint on 2026-10-04 (2.6.3),
59
+ 365 days at limit 200 (306 CVEs) kept 5 fields a row in the text, neither of those two among
60
+ them. A page of 50 comes back whole: 27,514 characters for production's 50 newest.
61
+ - `check_affected` keeps `ransomware` and `epss_score` on every match at every cut level, the
62
+ two fields `am_i_affected` lists for each match. In 2.6.3 a list at the API's cap of 200
63
+ matches kept `cve_id`, `kev_listed`, `effective_score`, `effective_severity` and
64
+ `score_assessed`: hosted, `linux_kernel` 5.10.0 had 20 CISA-KEV-listed matches and 1 with
65
+ known ransomware use, named nowhere in the text. Since 200 such matches with
66
+ `effective_severity` as well come to 33,240 characters, past the budget on their own, the
67
+ leanest level leaves `effective_severity` out. The `excluded` and `undetermined` samples now
68
+ keep their first 10 entries in the text (`excluded_count` and `undetermined_count` count every
69
+ entry), so every match still stays in the text. The description's "at least" list says so.
70
+ - `sbom_review` sends the purls not sent again from `data.not_sent_purls` only where the text
71
+ holds that list whole. Where the note says the text cuts it (to its first 10), the model
72
+ rebuilds the purls from the document instead: its distinct purls from the position the note
73
+ gives, in the order the note gives. `check_sbom`'s note now gives that position, and the order
74
+ it counts the input's purls in (a CycloneDX document's components depth first, each one's
75
+ nested components right after it, each purl at its first place), whenever purls are not sent,
76
+ not only when the text cuts the list. The prompt also says what to do when the note says rows
77
+ are left out of the text: list the rows the text carries, and give the rest as
78
+ `data.summary`'s counts.
79
+ - `sbom_review` reads each affected CVE's fixed versions from `cve_intel`, called beside
80
+ `get_cve` (`fixed_version` of its `affected_packages` and `fixed_versions` rows for the
81
+ component's package). In 2.6.3 it asked for "fixed versions where a result gives them", and
82
+ `check_sbom`'s text keeps `match_reason`, the advisory interval that names the fixed version,
83
+ only at its first cut level: the 50 components of a Juice Shop SBOM are cut past it.
84
+ - `sbom_review` never gives a fixed version that is not strictly greater than the component's
85
+ installed version, compared in the ecosystem's version order (where the model is not sure of
86
+ the order, it says so and gives none). It takes the end of the advisory interval in
87
+ `match_reason` first, where the text keeps it, and otherwise `cve_intel`'s `fixed_version`,
88
+ said to be one value per package, the advisory's last range's, which need not be the fix on
89
+ the component's branch (#2817). Where no version qualifies, the line says the results give no
90
+ fixed version for the component's branch and points to the CVE's advisory (`get_cve`'s
91
+ references). Read through the hosted endpoint on 2026-10-04 (2.6.3), `cve_intel` gives
92
+ log4j-core 2.12.2 for CVE-2021-44228, and `check_sbom`'s matches for 2.14.1 say it "is listed
93
+ verbatim in the advisory's affected versions", so the entry above alone had the model give
94
+ 2.12.2 as the fix for 2.14.1. The prompt also reads each component's ecosystem, package and version
95
+ from its purl, which every row the text keeps carries; the leanest cut, reached at 2,000 purls,
96
+ leaves the row's own fields out.
97
+ It maps a purl as `check_sbom`'s matcher does: the sixteen purl types with an ecosystem (`hex`,
98
+ `pub`, `swift`, `hackage`, `cran`, `bitnami`, `conan` and `githubact` among them, which the
99
+ first wording left out), `swift` and `githubact` named with their namespace as `npm`, `golang`
100
+ and `composer` are, and `deb`, `apk`, `alpine` and `rpm` by a `distro` qualifier of
101
+ `debian-N` (its major release), `ubuntu-V` or `alpine-X.Y` (cut to major.minor); for any other
102
+ the ecosystem is unknown, and the fix list gives no version for it.
103
+ - `triage_cve`'s patch line says that `cve_intel`'s `fixed_version` is one value per package, the
104
+ advisory's last range's, and is given as what `cve_intel` records for that package, never as
105
+ the fix for every affected branch; for a branch's fix it points to `get_cve`'s references
106
+ tagged Vendor Advisory or Patch and to the vendor advisories (#2817). The line had asked for
107
+ "fixed versions" alone, with no installed version to hold them against, so for CVE-2021-44228
108
+ it could give log4j-core 2.12.2, `cve_intel`'s one value on 2026-10-04, as the patch, below the
109
+ affected 2.14.1.
110
+ - `triage_cve` reads CISA's due date, `kev_due_date`, from `get_cve`'s record and no longer calls
111
+ `kev_recent`. Its step 6 read it from a `kev_recent` page of the CVE's `kev_added_date`, with no
112
+ limit and no paging. Read through the hosted endpoint on 2026-10-04 (2.6.3), the page for
113
+ 2021-11-03 held 50 of the 287 CVEs CISA added that day, and not CVE-2021-40444, one of them;
114
+ `get_cve` for CVE-2021-40444 carried its `kev_due_date` in its first text block.
115
+ - `cve://{cve_id}` stays under the 60,000-character ceiling. Its `data` is cut as `get_cve`'s
116
+ first text block is, laid out the same way, after the envelope. In 2.6.3 it was `get_cve`'s
117
+ structured result pretty-printed whole: 86,559 characters for CVE-2021-44228 on the hosted
118
+ endpoint, against 23,705 now on the suite's production-shaped answer. Its notes no longer carry
119
+ `get_cve`'s `TEXT CUT` sentences, which describe a first text block the resource does not have
120
+ (they said `cpe_match` kept 64 entries while the resource carried all 396). When `data` is cut,
121
+ the notes end with the resource's own sentence instead, starting `DATA CUT`, which names
122
+ `get_cve`'s `structuredContent.data` as the record whole. A record that fits is unchanged,
123
+ character for character. The resource's description and the methodology say so (#2801).
124
+ - No note promises a record or a component's matches "whole" from a tool whose text is cut too.
125
+ The `TEXT CUT` sentences of `search_cves`, `kev_recent`, `get_cwe` and `check_affected` say that
126
+ `get_cve` returns a record whole in its `structuredContent.data` and, in its first text block,
127
+ with every field, each list cut to its first entries past 30,000 characters. `check_sbom`'s
128
+ sentence says the same of `check_affected` and a component's matches, and the vendor-advisory
129
+ tools' of `get_vendor_advisory`. `get_cve`'s description and the README begin "One CVE's
130
+ record", no longer "Full record for one CVE". The source comments and the README no longer say
131
+ that `content[0]` always equals `data`. The 2.6.3 entry below gives the sizes production's record
132
+ holds for that release (26,011, 27,777, 28,793, 31,583 and 23,028), measured after #2729, in
133
+ place of the 25,951, 27,717, 28,733, 31,588 and 19,917 measured before it (#2802).
134
+ - Repository only, not in the package (#2800, #2799):
135
+ - Every production-shaped case's text is held within 50 characters of its recorded size, in
136
+ either direction. A text that shrinks is re-measured as one that grows is.
137
+ - The fields each description says a cut row keeps "at least" are read from the description
138
+ and checked on every row of every production-shaped case, for `search_cves`, `get_cwe`,
139
+ `kev_recent`, `check_affected` (its samples too), `check_sbom` and the vendor-advisory lists,
140
+ and at least one case of each tool must be cut. No answer the API serves reaches the cut of
141
+ `get_cwe` (each row's description is cut to 240 characters at the source) or of
142
+ `vendor_advisories_for_cve` (at most 20 short rows), so each has a forced case: production's
143
+ rows made longer. `get_cve` is held to keep every field. Removing `severity`, `cvss_v3_score`,
144
+ `echelongraph_score`, `score_assessed`, `epss_score` and `kev_listed` from `search_cves`'s
145
+ cut levels, which left 2.6.3's suite green, now fails it, as does dropping a field from
146
+ `get_cwe`'s.
147
+ - Each prompt's tool calls are made as the prompt says, at its largest input, on
148
+ production-shaped answers, and every field the prompt names must be in the text a client
149
+ that passes only `content` shows the model. Every tool a prompt names must be one of those
150
+ calls.
151
+ - A new test makes each prompt's tool calls as the prompt says, at its largest input, on
152
+ production-shaped answers, and checks every field the prompt names in the text: 365 days of
153
+ KEV additions, `check_affected` at its cap of 200 matches (both lookup paths), a 2,000-purl
154
+ SBOM answered in full and rate-limited, and `triage_cve`'s six tools.
155
+ - That test reads `sbom_review`'s `ecosystem`, `package` and `version` as fields: on a row whose
156
+ text leaves them out, reading them from the purl as the prompt says must give the answer's.
157
+ On production's `cve_intel` for CVE-2021-44228 and production's `check_sbom` answer for
158
+ log4j-core 2.14.1 (captured 2026-10-04), the prompt's fix rule must offer no fixed version,
159
+ 2.12.2 among the candidates, and on Juice Shop's lodash 4.17.19 it must offer 4.17.21 (#2817).
160
+ - `cve_summary` no longer fails when the `poller` block changes JSON type: a poller field the
161
+ answer sends in a type other than the one its outputSchema describes (`interval` as a Go
162
+ duration such as 1200000000000, `last_poll_at` as unix seconds, `cves_ingested` as a string) is
163
+ left out of `data` and named in the note, and a `poller` that is neither a JSON object nor null
164
+ is left out whole. `summary` and the rest of the answer are relayed as sent, measured. From
165
+ 2.6.2 one such field failed the whole call with `unexpected_shape` and withheld `summary.total`,
166
+ and an installed 2.6.2 or 2.6.3 still does: upgrade. The tool's description, the
167
+ `echelongraph://methodology` resource and the README say that `cve_summary`'s `data` is the
168
+ API's JSON less what the note names as left out (#2771).
169
+ - `echelongraph://sources` says what the NVD poller's `poll_count` and `poll_errors` are whenever
170
+ it relays either: that one instance's counters since it last started, zeroed on every restart,
171
+ never the feed's reliability. The resource's description and the README name all four fields it
172
+ relays. A `poller` the answer sends as something other than a JSON object is named as such,
173
+ never as a missing block (#2770).
174
+ - package.json's `description` fits the 255 characters npm's registry serves, so npm search,
175
+ `npm view` and the package page show it whole, with the per-CVE exposure footprint, its Shodan
176
+ attribution and Shodan's ownership sentence; through 2.6.3 npm cut the 443-character text
177
+ mid-phrase. A test holds it to 255 characters and whole sentences (#2779).
178
+ - package.json's `keywords`, and the public repository's GitHub topics, are held to the
179
+ description's wording rules by a test: no claim the package's own copy is barred from, no
180
+ ip:port service count named as machines, and nothing exclusive or comparative (#2780).
181
+ - Tests: check_sbom's description and the README's privacy section are pinned to saying that over
182
+ the hosted endpoint the document is the request body and reaches EchelonGraph's server, which
183
+ neither logs nor keeps it, and to never denying that the document is sent (#2796). The denials
184
+ are one list, `test/document-denials.mjs`, that the monorepo's /pulse/mcp check reads too, in
185
+ every noun `check_sbom` uses for its input; the README check's own list had passed "The server
186
+ never sends the document.", "The document is processed locally." and "Only the purls are sent."
187
+ (#2796, #2795).
188
+ - `check_sbom`'s `sbom` argument said "its purls are read here and only they are sent", which over
189
+ the hosted endpoint is not so: there the whole document is the request body. It now says the
190
+ purls are read by this MCP server and only they are sent to the API, and that over the hosted
191
+ endpoint (mcp.echelongraph.io) the document is the request body, in the description's own words.
192
+ The README Tools table's `check_sbom` row, which ended "the document is not.", says the same.
193
+ Tests: every string of `check_sbom`'s tools/list entry, its argument and output descriptions
194
+ among them, and that README row are now held to the denial list, which also reads "not sent on"
195
+ as true only when the clause ends there, and catches more wordings of where the document stays
196
+ or is processed (#2796).
197
+
9
198
  ## 2.6.3 — 2026-10-04
10
199
 
11
200
  - `exposure_radar` relays the observation window each count was measured over, where the API now
@@ -50,8 +239,8 @@ from the commit tagged `v<version>`.
50
239
  was 133,300 characters of text, `get_cve` CVE-2021-44228 80,897, `check_affected` openssl 3.0.0
51
240
  156,058 (the Linux kernel at 5.10.0, 364,408), `check_sbom` on a 50-component SBOM 112,820,
52
241
  `kev_recent` at limit 200 105,925 and `search_vendor_advisories` at limit 50 103,147, against
53
- the 60,000-character ceiling; the same calls now measure 13,513, 22,960, 25,951 (27,717),
54
- 28,733, 31,588 and 19,917. The JSON is first laid out one row a line, which loses nothing; past
242
+ the 60,000-character ceiling; the same calls now measure 13,513, 22,960, 26,011 (27,777),
243
+ 28,793, 31,583 and 23,028. The JSON is first laid out one row a line, which loses nothing; past
55
244
  that, a list tool cuts each row to the fields it names (the description cut to 200 characters,
56
245
  every long string ending in "…"), then to fewer, and leaves rows out only when its leanest cut
57
246
  is still too long (`check_sbom` leaves out `not_affected` rows first); `get_cve` keeps the first
package/README.md CHANGED
@@ -28,6 +28,8 @@ API call a tool needs to answer.
28
28
  Registry: `io.echelongraph/echelongraph-mcp` · Docs: <https://echelongraph.io/pulse/mcp>
29
29
  - Source: <https://github.com/echelongraph/echelongraph-mcp> · Changes:
30
30
  [CHANGELOG.md](CHANGELOG.md) · Security: [SECURITY.md](SECURITY.md)
31
+ - Also listed on: [Smithery](https://smithery.ai/servers/echelongraph/echelongraph-mcp) ·
32
+ [Glama](https://glama.ai/mcp/servers/echelongraph/echelongraph-mcp)
31
33
 
32
34
  ## Quick start
33
35
 
@@ -38,20 +40,83 @@ have on record for it?"*
38
40
 
39
41
  ### Claude Desktop
40
42
 
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:
43
+ Two ways in. The first needs nothing installed; the second runs the server on your machine.
44
+
45
+ **1. Custom connector (no install).** Claude Desktop uses your claude.ai account's connectors:
46
+ Customize → Connectors → **+ Add** → **Add custom connector**. Name it `EchelonGraph`, paste
47
+ `https://mcp.echelongraph.io/mcp`, choose **No sign in**, and add it. No Node.js, no PATH and no
48
+ JSON to edit. A connector added on claude.ai shows up in Claude Desktop too, and the plan rules
49
+ are claude.ai's (see "Remote (no install)" below).
50
+
51
+ **2. Local server through `npx`.**
52
+
53
+ 1. **Check Node.js.** In Terminal (macOS) or PowerShell (Windows), run `node -v`. It must print
54
+ `v20` or later. If it prints an older version or `command not found`, install the LTS release
55
+ from <https://nodejs.org>, or on macOS with Homebrew run `brew install node`.
56
+ 2. **Open the config file in a plain-text editor.** Settings → Developer → Edit Config shows
57
+ `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`;
58
+ Windows: `%APPDATA%\Claude\claude_desktop_config.json`). Open it in a plain-text editor such as
59
+ VS Code, Notepad or `nano`. TextEdit on macOS turns `"` into curly quotes (`“ ”`) when Smart
60
+ Quotes is on, and the JSON then breaks without an error: turn it off under Edit → Substitutions
61
+ → Smart Quotes, or use another editor.
62
+ 3. **Merge, do not replace.** The file usually exists already and holds Claude Desktop's own
63
+ settings (for example a `"preferences"` object). Keep everything in it. Add an `"mcpServers"`
64
+ key at the top level, next to the keys already there; if `"mcpServers"` is already there, add
65
+ the `"echelongraph"` entry inside it. Mind the comma between entries. The result looks like
66
+ this, with your own settings where `preferences` is:
67
+
68
+ ```json
69
+ {
70
+ "preferences": {
71
+ "…": "the settings already in your file stay as they are"
72
+ },
73
+ "mcpServers": {
74
+ "echelongraph": {
75
+ "command": "npx",
76
+ "args": ["-y", "echelongraph-mcp"]
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ To check that the file parses, run
83
+ `node -e "JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'))" "<path to the file>"`:
84
+ no output means valid JSON.
85
+ 4. **Quit fully, then reopen.** Closing the window leaves the old config loaded. On macOS choose
86
+ Claude → Quit Claude (⌘Q); on Windows right-click the Claude icon in the system tray and choose
87
+ Quit. The first start can take longer while `npx` downloads the package.
88
+ 5. **Check that it worked.** Settings → Developer lists `echelongraph` as running, and the tools
89
+ menu in the chat box lists EchelonGraph's 14 tools. Then ask the question at the top of this
90
+ section.
91
+
92
+ #### Troubleshooting Claude Desktop
93
+
94
+ - **`spawn npx ENOENT`** (or the server shows as failed). On macOS, an app opened from the Dock
95
+ does not get your shell's PATH, so Claude Desktop cannot find `npx` where Homebrew (Apple
96
+ silicon: `/opt/homebrew/bin`; Intel: `/usr/local/bin`) or nvm put it. Run `which npx` in
97
+ Terminal, then give Claude Desktop that absolute path as `"command"`, and the folder it is in as
98
+ `PATH` under `"env"` (`npx` starts `node` from that same folder). With Homebrew on Apple silicon:
44
99
 
45
- ```json
46
- {
47
- "mcpServers": {
48
- "echelongraph": {
49
- "command": "npx",
50
- "args": ["-y", "echelongraph-mcp"]
100
+ ```json
101
+ {
102
+ "mcpServers": {
103
+ "echelongraph": {
104
+ "command": "/opt/homebrew/bin/npx",
105
+ "args": ["-y", "echelongraph-mcp"],
106
+ "env": { "PATH": "/opt/homebrew/bin:/usr/bin:/bin" }
107
+ }
51
108
  }
52
109
  }
53
- }
54
- ```
110
+ ```
111
+
112
+ With nvm, use the full path `which npx` prints (JSON does not expand `~`). Then quit fully and
113
+ reopen. On Windows, install Node.js with the installer from nodejs.org, which adds `npx` to the
114
+ system PATH, then quit Claude Desktop fully and reopen it.
115
+ - **The server does not appear at all.** The file is not valid JSON (a missing comma, or curly
116
+ quotes), or the app was not fully quit: see steps 2 to 4.
117
+ - **Logs.** macOS: `~/Library/Logs/Claude/mcp-server-echelongraph.log` (this server's output) and
118
+ `~/Library/Logs/Claude/mcp.log` (connections). Windows: `%APPDATA%\Claude\logs\`, with the same
119
+ file names.
55
120
 
56
121
  ### Claude Code
57
122
 
@@ -156,13 +221,13 @@ The example questions are ones a client can answer with that tool alone.
156
221
  |---|---|---|---|
157
222
  | `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
223
  | `search_cves` | Search CVEs | Search/filter CVEs (severity, min CVSS, text, sort) with EchelonGraph scores and `score_assessed`; page with `limit` and `offset`; 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?* |
224
+ | `get_cve` | CVE detail | One CVE's record: 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
225
  | `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
226
  | `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
227
  | `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
228
  | `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
229
  | `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 2,000 distinct purls, or a CycloneDX JSON or SPDX JSON document: the purls are read from it by the MCP server (on your machine when it runs from npm) and only they are sent to the API, in POST bodies of at most 200 each, one after another; the document is not. When the API's per-component rate limit answers 429, the tool waits the `Retry-After` it names, within 50 seconds per call; past that it answers what it has, with the purls not sent counted in `coverage.not_sent`, the reason in `coverage.not_sent_reason`, and the list in `data.not_sent_purls`, never dropped silently. 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.* |
230
+ | `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 2,000 distinct purls, or a CycloneDX JSON or SPDX JSON document: the purls are read from it by the MCP server and only they are sent to the API, in POST bodies of at most 200 each, one after another; the document itself is not sent on. Run from npm, the server is on your machine; over the hosted endpoint (`mcp.echelongraph.io`) it is EchelonGraph's, and the document is the request body. When the API's per-component rate limit answers 429, the tool waits the `Retry-After` it names, within 50 seconds per call; past that it answers what it has, with the purls not sent counted in `coverage.not_sent`, the reason in `coverage.not_sent_reason`, and the list in `data.not_sent_purls`, never dropped silently. 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
231
  | `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
232
  | `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
233
  | `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?* |
@@ -195,8 +260,17 @@ it is neither a count of CVEs rated None nor the count of CVEs with no severity,
195
260
  `summary.total` and the other counts leave them out, and they are withdrawn records, never
196
261
  vulnerabilities.
197
262
 
198
- The tool relays the API's JSON as it was sent. When `summary.none`, `summary.nvd_none` or
199
- `summary.rejected` is above zero, the note says what it counts, with the number from the answer.
263
+ `poller` holds the in-memory counters of the NVD poller of the one API instance that answered,
264
+ counted since that instance last started and zeroed on every restart: they describe that instance,
265
+ never the feed's size, intake, reliability or freshness, and the note says so whenever the answer
266
+ carries `poller`. A `poller` field the answer sends in a JSON type other than the one the
267
+ outputSchema describes is left out and named in the note, and a `poller` that is neither a JSON
268
+ object nor null is left out whole; `summary` is relayed either way, so a change to that diagnostic
269
+ block never withholds the counts.
270
+
271
+ The tool relays the API's JSON as it was sent, less a `poller` field left out as above. When
272
+ `summary.none`, `summary.nvd_none` or `summary.rejected` is above zero, the note says what it
273
+ counts, with the number from the answer.
200
274
  The note says the five NVD counts add up to `summary.total` only when the answer's do, and says
201
275
  nothing about a field the answer does not carry.
202
276
 
@@ -470,18 +544,18 @@ never "not affected", and each figure cites its `measured_at`. A prompt makes no
470
544
 
471
545
  | Prompt | Arguments | What it asks the model to do |
472
546
  |---|---|---|
473
- | `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. |
474
- | `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. |
547
+ | `triage_cve` | `cve_id` | `get_cve` (with CISA's due date, `kev_due_date`, when the CVE is KEV-listed), `cve_intel`, `vendor_advisories_for_cve`, `epss_history` and `cve_exposure`, then a decision template: exploited, likelihood, reachable, patch, deadline, decision. The patch line gives `cve_intel`'s `fixed_version` as what it records for the package, one per package (the advisory's last range's), never as the fix for every affected branch, and points to `get_cve`'s references tagged Vendor Advisory or Patch for a branch's fix. |
548
+ | `kev_weekly_brief` | `days` (1 to 365, default 7) | Work out the start date from `days`, page through `kev_recent` from it 50 rows a page, and write a brief grouped by vendor with ransomware flags and due dates. |
475
549
  | `am_i_affected` | `product`, or `ecosystem` and `package`; `version` | `check_affected`, read `assessed` before `count`, with the not-assessed wording. |
476
- | `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. |
550
+ | `sbom_review` | `sbom` (CycloneDX or SPDX JSON text, up to 5,000,000 characters) | Pass the document to `check_sbom` (and, once more, the purls a rate limit or the time budget left unsent: `data.not_sent_purls`, or, where the text cuts that list, the document's purls from the position and in the order its note gives), read each component's ecosystem, package and version from its purl, as `check_sbom` maps them (every purl type it maps, and `deb`, `apk`, `alpine` and `rpm` by a `distro` qualifier naming a Debian, Ubuntu or Alpine release; any other is unknown, and given no fixed version), look up each affected CVE with `get_cve` and `cve_intel`, and write a fix list ordered by CISA-KEV listing, then EPSS, then score, each key stated. A fixed version is given where it is above the component's installed version: the advisory interval in `check_sbom`'s `match_reason` first, else `cve_intel`'s `fixed_version`, which is one per package (the advisory's last range's) and need not be the fix on the component's branch; otherwise the line says the results give no fixed version for that branch and points to the CVE's advisory. |
477
551
 
478
552
  ## Resources
479
553
 
480
554
  | Resource | What it holds |
481
555
  |---|---|
482
556
  | `echelongraph://methodology` | What each envelope field and `state` value means, and how each tool measures (Markdown). |
483
- | `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). |
484
- | `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. |
557
+ | `echelongraph://sources` | What the API reports about its feeds when the resource is read: the NVD poller's `interval`, `last_poll_at`, `poll_count` and `poll_errors`, all of the one instance that answered (from `/api/v1/public/cves/summary`; the two counts are that instance's since it last started, zeroed on every restart, never the feed's reliability), 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). |
558
+ | `cve://{cve_id}` | One CVE as `get_cve` returns it: the structured result, envelope and record, as JSON. Past 30,000 characters of JSON, `data` is cut as `get_cve`'s first text block is, and a note starting `DATA CUT` says what the cut leaves out; `get_cve`'s `structuredContent.data` holds the record whole. An ID that is not a CVE ID is refused before any request; a CVE the API has no record of is not found. |
485
559
 
486
560
  ## What a result means
487
561
 
@@ -494,8 +568,10 @@ outage for an all-clear:
494
568
  the structured result as JSON, less what the first two already say (below). When the
495
569
  feed genuinely holds nothing for the query the note says so in words ("we looked and
496
570
  found nothing … not a lookup failure"), because a measured zero is a measurement. The
497
- exception to verbatim is `exposure_radar`: each radar is cut to the fields listed above, and
498
- each `kev_exposure.newest_kev` row gains an `exposure_state`. Its `shadow_ai` counts are
571
+ exceptions to verbatim are `cve_summary`, which leaves out a `poller` field the answer sends
572
+ in a JSON type other than its outputSchema's (or a `poller` that is neither a JSON object nor
573
+ null) and names it in the note, and `exposure_radar`: each radar is cut to the fields listed
574
+ above, and each `kev_exposure.newest_kev` row gains an `exposure_state`. Its `shadow_ai` counts are
499
575
  regrouped as above, and its `poller` block carries only `running` and `last_run_at`, or is dropped when it
500
576
  carries no real completion time. The block's other fields describe the server instance that
501
577
  answered, not the radar. Its `last_run_at` is when the radar's leader last
@@ -538,17 +614,24 @@ most kept first (`search_cves` keeps `cve_id`, `severity`, `cvss_v3_score`, `ech
538
614
  `score_assessed`, `epss_score`, `kev_listed` and the first 200 characters of the description,
539
615
  among others), with every long string ending in "…". A list beside the rows is cut before any
540
616
  row field the first level keeps, and before any row leaves the text: `check_affected`'s
541
- `excluded` and `undetermined` samples keep each entry's `cve_id` and `reason`, and its `cve_ids`
542
- (each match's `cve_id`, in order) its first 10, so that every match stays in the text, and `check_sbom`'s `not_sent_purls` keeps its first 10, the note saying from
543
- which position of the input the purls not sent run, so that a second call can send them without
544
- reading the list. When even the leanest cut is too long, rows are left out of the text
617
+ `excluded` and `undetermined` samples keep their first 10 entries, each its `cve_id` and
618
+ `reason`, and its `cve_ids` (each match's `cve_id`, in order) its first 10, so that every match
619
+ stays in the text, each with at least `cve_id`, `kev_listed`, `ransomware`, `epss_score`,
620
+ `effective_score` and `score_assessed`, and `check_sbom`'s `not_sent_purls` keeps its first 10, the note saying from
621
+ which position of the input the purls not sent run, and in what order the input's purls are
622
+ counted, so that a second call can send them without reading the list. When even the leanest cut is too long, rows are left out of the text
545
623
  (`check_sbom` leaves out the `not_affected` rows first, then the `not_assessed` ones), and the
546
624
  note says how to read them: the `offset` to call next, or a smaller `limit`. Any other answer
547
625
  keeps the first entries of each of its lists (`get_cve`'s `cpe_match` and `references`). The note
548
626
  then ends with one sentence starting `TEXT CUT` that says what the first text block leaves out
549
- and which tool returns a row whole (`get_cve`, `get_vendor_advisory`, `check_affected`, and
550
- `cve_intel` a CVE's fixed versions). An answer that fits is sent as before, pretty-printed and
551
- whole. The same answer is always cut the same way.
627
+ and which tool returns a row (`get_cve`, `get_vendor_advisory`, `check_affected`, and
628
+ `cve_intel` a CVE's fixed versions): whole in that tool's `structuredContent.data`, since its own
629
+ first text block is cut past 30,000 characters too. An answer that fits is sent as before,
630
+ pretty-printed and whole. The same answer is always cut the same way. The `cve://{cve_id}`
631
+ resource cuts its `data` as `get_cve`'s first text block is cut, and says so in a note starting
632
+ `DATA CUT`. The prompts ask only for what the text keeps: `kev_weekly_brief` reads `kev_recent`
633
+ 50 rows a page, which the text holds whole; `triage_cve` reads CISA's due date from `get_cve`;
634
+ and `sbom_review` reads fixed versions from `cve_intel`.
552
635
 
553
636
  ## Structured results
554
637
 
@@ -576,8 +659,8 @@ note's sentences, which are the text block just before it (on a failure, the mes
576
659
  with which `notes` ends: its `notes` holds only the sentences the envelope adds about itself,
577
660
  and is left out when there are none. It leaves out `method` too where that note quotes it verbatim, as
578
661
  `cve_exposure`'s does ("Method: …"). So the text blocks together carry the whole structured
579
- result, and cannot disagree with it. Before 2.2.0 this block repeated the whole note, so every
580
- note was sent twice.
662
+ result, but for what a `TEXT CUT` leaves out of `data` (see "Long answers"), and cannot disagree
663
+ with it. Before 2.2.0 this block repeated the whole note, so every note was sent twice.
581
664
 
582
665
  | `state` | What it means |
583
666
  |---|---|
@@ -622,7 +705,9 @@ The CVE feed tools' `freshness` is `null`: the feed's answers carry no time at w
622
705
  pollers last completed a poll for the feed as a whole.
623
706
 
624
707
  A success whose fields do not fit the tool's `outputSchema` (a field of a type the schema does
625
- not allow) is returned as a failure with `error.kind` `unexpected_shape`, never relayed. A
708
+ not allow) is returned as a failure with `error.kind` `unexpected_shape`, never relayed, with one
709
+ exception: a `cve_summary` `poller` field of another JSON type is left out of `data` and named in
710
+ the note, and the rest of the answer is relayed, `summary` with it. A
626
711
  field the API adds later is still relayed by every tool that relays the API's JSON as data, and
627
712
  `exposure_radar` and `cve_intel`, which relay a selection, leave it out and name what they leave out.
628
713
 
@@ -718,7 +803,10 @@ JSON-RPC error (code `-32029`) that names the limit and the seconds to wait. A r
718
803
  64 KiB is refused (HTTP 413, code `-32600`), except a `tools/call` of `check_sbom` or a
719
804
  `prompts/get` of `sbom_review`, which may be up to 6 MiB: enough for a CycloneDX or SPDX document
720
805
  at the tool's 5,000,000-character cap. Each server instance reads at most two such large bodies at
721
- once; a third is answered HTTP 503 with `Retry-After` and a JSON-RPC error (code `-32030`).
806
+ once; a third is answered HTTP 503 with `Retry-After` and a JSON-RPC error (code `-32030`). Each
807
+ instance serves at most 64 requests at once; the next waits up to 10 seconds for one to finish,
808
+ and is then answered the same way. A `subscriptions/listen` stream, which stays open until its
809
+ client leaves, is not one of the 64.
722
810
 
723
811
  ## Configuration
724
812
 
@@ -731,8 +819,12 @@ The npm package (stdio):
731
819
  | `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. |
732
820
 
733
821
  The HTTP entrypoint, `dist/http.js` (`npm run start:http`), serves the same tools over
734
- Streamable HTTP on `POST /mcp`, with a liveness check on `GET /health`. It reads the three
735
- variables above and:
822
+ Streamable HTTP on `POST /mcp`, with a liveness check on `GET /health`. In a container with
823
+ little memory, start it with V8's heap capped below the container's limit, as EchelonGraph's
824
+ hosted deployment does at half of 256 MiB (`node --max-old-space-size=128 --max-semi-space-size=16 dist/http.js`): Node
825
+ otherwise sizes its heap from the memory the system reports, which in a container can be the
826
+ host's, and the container is then killed before V8 collects.
827
+ It reads the three variables above and:
736
828
 
737
829
  | Env var | Default | Purpose |
738
830
  |---|---|---|
@@ -741,6 +833,8 @@ variables above and:
741
833
  | `MCP_MAX_BODY_BYTES` | `65536` | The largest request body it reads, except for an SBOM (below). |
742
834
  | `MCP_MAX_SBOM_BODY_BYTES` | `6291456` | The largest body of a `tools/call` of `check_sbom` or a `prompts/get` of `sbom_review`. |
743
835
  | `MCP_LARGE_BODY_SLOTS` | `2` | How many bodies over `MCP_MAX_BODY_BYTES` it reads and serves at once; past that, 503 with `Retry-After`. |
836
+ | `MCP_MAX_IN_FLIGHT` | `64` | How many requests it serves at once, not counting `subscriptions/listen` streams; the next, its body read, waits. |
837
+ | `MCP_ADMISSION_WAIT_MS` | `10000` | How long a request waits to be served before it is answered 503 with `Retry-After`. |
744
838
  | `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. |
745
839
 
746
840
  ## Develop