echelongraph-mcp 2.6.2 → 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,260 @@ 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
+
198
+ ## 2.6.3 — 2026-10-04
199
+
200
+ - `exposure_radar` relays the observation window each count was measured over, where the API now
201
+ serves one: `exposed_databases.window` (when our verifier last confirmed each host),
202
+ `leaked_credentials.window` (when each key was last seen) and `shadow_ai.confirmed_exposed.window`
203
+ (when our verifier's deciding probe ran). A window is relayed only when no counted row is undated;
204
+ otherwise the note says how many are, and a malformed window is left out and named. KEV-exposure
205
+ carries no observation time yet. `exposure_radar` stays `not_assessed` with `measured_at` null:
206
+ a window's end is not one observation time (#2438).
207
+ - `vendor_advisories_for_cve`, `search_vendor_advisories` and `get_vendor_advisory` read each
208
+ vendor's window from `GET /api/v1/public/vendor-advisories/coverage` beside their own request,
209
+ and relay it in `coverage` (`vendor_windows`, or `vendor_window` for the detail): the advisories
210
+ held, `earliest_vendor_published_at`, `latest_vendor_published_at` and `history_backfill`.
211
+ `vendor_advisories_for_cve` adds `cve_year` and `vendors_not_fully_held`, the vendors with no
212
+ advisory in the answer of which EchelonGraph holds none, whose earliest held advisory is dated
213
+ after 1 January of the year in the CVE ID, or whose history is still being read, and its note
214
+ names each with its window: an empty answer for CVE-2024-3400 can no longer be read as Palo Alto
215
+ having published none. Its description no longer says an empty answer means none of the polled
216
+ feeds names the CVE: it means none of the advisories EchelonGraph holds does. The search
217
+ and the detail name the vendors whose history is still being read. When the windows cannot be
218
+ read, the answer is still relayed, measured, with the windows null and a note saying so (#2729).
219
+ - `search_vendor_advisories`: a query of 1 or 2 characters matches whole words only
220
+ (`search_match` `word`), as the API now answers it, and the description and the note say so; a
221
+ search's total is counted to at most 1,000, and a capped total (`total_capped` true) is written
222
+ "1,000+" in the note, never as a count. `coverage` gains `total_capped` and `search_match`
223
+ (#2728).
224
+ - `check_sbom`'s 50-second budget bounds the whole call, not only when a batch may start: each
225
+ batch's request may take only what is left of it, and a batch still unanswered when the budget
226
+ runs out is cut off and its purls answered as not sent (`not_sent_reason` `time_budget`). Before,
227
+ a batch started at 49.9 s could run for the 15-second request timeout, past the 60-second limits
228
+ of the SDK's client and the hosted endpoint, which then answered nothing at all. A Retry-After
229
+ wait that ends at the budget's end, before any batch was answered, answers that 429 instead of
230
+ sending a request with no time left (#2756).
231
+ - Repository only, not in the package: the production synthetic probes `check_sbom` with 201
232
+ purls, so every run sends two batches, and fails the probe (`expectation_unmet`) unless both
233
+ were answered; core-backend's tests fail when `MAX_COMPONENTS`, `MAX_PURL_LEN` or
234
+ `API_COMPONENTS_PER_MINUTE` disagree with the API's own values, and every core-backend deploy runs
235
+ them before it builds (`scripts/check-outbound-identity.sh`'s table), refusing on drift (#2757).
236
+ - A success's first text block is cut to fit 30,000 characters when the API's JSON is longer, and
237
+ `structuredContent.data` still carries the answer whole. Through 2.6.2 that block was the JSON
238
+ whatever its size: on production's answers of 2026-10-04 a default `search_cves` for "openssl"
239
+ was 133,300 characters of text, `get_cve` CVE-2021-44228 80,897, `check_affected` openssl 3.0.0
240
+ 156,058 (the Linux kernel at 5.10.0, 364,408), `check_sbom` on a 50-component SBOM 112,820,
241
+ `kev_recent` at limit 200 105,925 and `search_vendor_advisories` at limit 50 103,147, against
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
244
+ that, a list tool cuts each row to the fields it names (the description cut to 200 characters,
245
+ every long string ending in "…"), then to fewer, and leaves rows out only when its leanest cut
246
+ is still too long (`check_sbom` leaves out `not_affected` rows first); `get_cve` keeps the first
247
+ entries of `cpe_match` and `references`. The note then ends with a sentence starting
248
+ `TEXT CUT` that names what the first text block leaves out, which tool returns a row whole, and,
249
+ when rows are left out, the `offset` or page size at which they fit. An answer that fits is sent
250
+ as before. Every tool description, the server instructions and the methodology resource say so
251
+ (#2783).
252
+ - `search_cves` takes `offset` (0 to 10,000), so the next page a cut text names can be read (#2783).
253
+ - A list beside the rows is cut before the rows are: `check_affected`'s `excluded` and
254
+ `undetermined` samples keep each entry's `cve_id` and `reason`, and `cve_ids` (each match's
255
+ `cve_id`, in order) its first 10, so every match stays in the text (`flash_player` 10.0.0, 200
256
+ matches and 50 excluded entries, had 75 matches left out of it, two of them KEV-listed), and `check_sbom`'s `not_sent_purls` keeps its first 10, with the note
257
+ saying from which position of the input the purls not sent run. A 2,000-purl `check_sbom` call
258
+ that the API's budget stops after 1,200 now keeps the affected rows before the clean ones in the
259
+ text. A cut `check_affected` match keeps `match_reason` (a registry match's advisory interval,
260
+ which names the fixed version) where it fits, and the note names `cve_intel` for fixed versions
261
+ (#2783).
262
+
9
263
  ## 2.6.2 — 2026-10-04
10
264
 
11
265
  - `kev_recent` reads an RFC 3339 `since` or `until` as the date written in it, dropping the time