echelongraph-mcp 2.7.2 → 3.0.2

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,225 @@ 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
+ ## 3.0.2 — 2026-10-17
10
+
11
+ 3.0.1 was tagged but never published to npm: its release run's memory test
12
+ (`test/http-memory.test.mjs`) failed. With two 2,000-purl documents and 64 other requests in
13
+ flight, the hosted endpoint needed the whole of its image's 128 MiB heap, and V8 aborted. 3.0.2 is
14
+ 3.0.1's changes with that fixed (the first entry).
15
+
16
+ - **The hosted image caps V8's old space at 104 MiB, not 128** (#3132, Dockerfile.http). With the
17
+ schemas built once, the memory test's live set fits in 80 MiB, and at 128 V8 let the burst's
18
+ garbage stand until the container peaked at 256-258 MB of its 268 MB. At 104 the peak is
19
+ 235-241 MB with about 24 MiB of heap to spare (3.0.0 at 128: 238-252 MB). The start-up line's
20
+ `heap_limit_mb` now reads 152 (104 plus the young generation), not 176.
21
+
22
+ - **The hosted endpoint builds each tool's output schema once, not once per request** (#3132).
23
+ It serves every request with a server of its own, and 3.0.1's closed copy of each success
24
+ branch, the check behind the open `outputSchema` (below), doubled the output schemas each one
25
+ built: 1,371 KB of heap a server, against 3.0.0's 1,168. The memory test's two SBOMs were still
26
+ at their first batches when V8 aborted, so it was not their cut that ran out. Each schema is now
27
+ built once per process, 611 KB a server, and the memory test passes with an 80 MiB heap, where
28
+ 3.0.0 needed 112 and 3.0.1 128. check_sbom's and scan_manifest's structured cut also measures a
29
+ thinned row and lets it go, and thins again only the rows it keeps: on that test's 2,000-row
30
+ answer it allocated 108 MB in 3.0.1, 18 MB in 3.0.0 and 24 MB now. No answer changes: every
31
+ call of the suite returns the same text and `structuredContent` as 3.0.1.
32
+ - **check_sbom and scan_manifest keep every row before any row leaves `data`** (#3132). In 3.0.0
33
+ the structured cut had one level for a row, which kept every match, so a row still large once
34
+ thinned left `data`, and every row after it went with it. Hosted 3.0.0 kept 1 of 5 rows for five
35
+ Debian 12 purls, all affected (linux-libc-dev 6.1.55-1 has 200 matches). It kept 2 of 51 rows for
36
+ Juice Shop 11.1.2 with `pkg:composer/magento/community-edition@2.3.0` at index 2. For that purl
37
+ alone it kept no row, and the note said "check 0 or fewer purls per call". Now, past the thinned
38
+ level, every row's matches are capped at the same number, the most that fit. A row with more
39
+ keeps its first matches (highest `effective_score` first) and carries `matches_truncated`: its
40
+ total, the number returned, and the KEV-listed and ransomware CVE IDs and the `fixed_in` versions
41
+ of all its matches. Next, each match is cut to `cve_id`, `kev_listed`, `ransomware`, `fixed_in`
42
+ and `effective_score`, the row keeping `matched_package`, `matched_via` and an upstream row's
43
+ `matched_version_from` and `purl_name`. Only when that cannot keep every row is each row cut to
44
+ `index`, `purl`, `verdict`, `not_assessed_reason` and `cve_ids`. Rows leave only when even that
45
+ cannot keep them all, clean rows first as before, and they leave at the compact match level, at
46
+ most 10 matches a row, so each row kept still says its KEV flags and `fixed_in`. Once the cut is
47
+ settled, the rows kept take back detail in input order while room is left. Replayed on those
48
+ answers, the three cases keep 5 of 5 rows, 51 of 51 and 1 of 1. On the production synthetic's
49
+ 201-purl probe, all affected, 3.0.0 kept 43 rows; this release keeps 54 compact ones, the
50
+ log4j-core 2.14.1 row with CVE-2021-44228 (`fixed_in` 2.15.0). The note no longer says
51
+ "0 or fewer".
52
+ - **`not_sent_purls` is kept whole or left out, never cut short** (#3132). It is the list a client
53
+ sends again. When a `DATA CUT` leaves it out, check_sbom's note gives the purls not sent as
54
+ positions in the input: `coverage.sent` + 1 to `coverage.distinct_purls`, in the order the note
55
+ states. scan_manifest's positions would count purls read from the files, which a client cannot
56
+ rebuild, so its note says the same call with `full: true` returns the list instead.
57
+ - **cve_remediation drops a vendor advisory only after `patches` and `fixed_branches`** (#3139).
58
+ CVE-2023-44487's default answer kept 2 of its 20 advisories, while `fixed_branches` (50 rows) and
59
+ `patches` (30) stayed whole. Now every advisory is kept, thinned. Then `fixed_branches` is thinned
60
+ (to the package, its range, `fixed_version` and each range's `introduced`, `fixed` and
61
+ `last_affected`; each range's `source` and `advisory_id` go), and each package's ranges are
62
+ capped at the most that fit, a package so cut carrying `fixed_branches_truncated` (total and
63
+ returned). Then `patches` are thinned (to `vendor`, `patch_url` and `published_at`), every one
64
+ kept, and `fix_references` is cut. Only then do `patches` rows leave, then `fixed_branches` rows.
65
+ Room left goes back to the capped ranges. Replayed, that answer keeps all 20 advisories, all 50
66
+ `fixed_branches` and all 30 `patches`, each thinned, and 17 of 20 `fix_references`, at 29,931
67
+ characters. When
68
+ advisories do leave, the `DATA CUT` note names `vendor_advisories_for_cve`, which lists every
69
+ one.
70
+ - **`patch_available`'s schema text says what `true` beside an empty `patch_evidence` means**
71
+ (#3138). It means evidence of a fix once held has since been withdrawn: the flag is never set
72
+ back to false. `remediation.fix_status` is then the current reading. The text used to say `true`
73
+ means EchelonGraph holds evidence of a fix, while hosted get_cve CVE-2026-1281 answered `true`
74
+ beside `[]` and `none_confirmed`. get_cve's and search_cves' descriptions now say `true` means
75
+ EchelonGraph holds, or once held, fix evidence for the CVE, and `patch_evidence` is what it holds
76
+ now.
77
+ - **A success's `outputSchema` admits top-level fields it does not name** (#3121). A client
78
+ validates answers against the tool list it read, and the hosted endpoint never tells it to read
79
+ again. In 3.0.0 a Claude Code session that listed the tools under 2.7.2 failed every tool's
80
+ call: "data must NOT have additional properties", from `data_truncated`, which 3.0.0 added to
81
+ every success. A field added at the top level in a minor release no longer breaks such a client.
82
+ The failure branch and each tool's `data` schema stay closed, so a field added inside `data`
83
+ still needs a reconnect, and the server still checks its own answers against the closed success
84
+ branch (`test/checked-strict.test.mjs`). If you see "does not match the tool's output schema" after an upgrade, reconnect
85
+ the client (README, Versioning).
86
+ - `test/text-bound-live.mjs --hosted` calls a 3.0.0 or later endpoint with `full: true`. It
87
+ refuses to replay a cut `data` as the API's answer (#3140). Without `full: true`, get_cve
88
+ CVE-2021-44228 replayed 77 of its 396 `cpe_match` entries. The vendor tools' `/coverage` is now
89
+ served from the case's fixture beside the replayed answer, not as that answer. The record
90
+ (`test/fixtures/text-bound-live.json`) was re-measured on hosted 3.0.0 and holds a structured size
91
+ for each of the 23 cases measured; scan_manifest's 64-package lock, cve_remediation and cve_intel
92
+ with `fixed_branches` are measured on production for the first time. Four fixtures were re-read
93
+ where production had moved more than 10% from them (search_cves, epss_history,
94
+ vendor_advisories_for_cve, and scan_manifest's lock, which now replays production's own rows).
95
+
96
+ - **The hosted endpoint's `mcp_handler_error` log names the cause of createMcpHandler's errors**
97
+ (#2923). 2.7.1's `error_class` matched only the legacy transport's messages, so all 78 production
98
+ lines read from 2026-10-06 were `other`. It now also matches the bundled SDK's "Rejected inbound
99
+ request (", "Rejected 2025-era request on a modern-only endpoint (", "Unsupported protocol
100
+ version" (code -32022, and the legacy transport's "Bad Request: " form) and "Missing required
101
+ client capabilities". As before, only the label and the numeric code are logged, never the
102
+ message.
103
+
104
+ ## 3.0.0 — 2026-10-11
105
+
106
+ A major version (README, Versioning): the structured result is now held to 30,000 characters by
107
+ default (the first entry below), so a program that read `structuredContent.data` whole gets it cut
108
+ on a large answer unless it passes `full: true`. A client that hands the model the text or the
109
+ structured result needs no change; it gets an answer where 2.7.x gave Claude Code a file path.
110
+
111
+ - **`fixed_in` is never a version the same CVE still affects** (#3074). `fixed_in`, a match's in
112
+ check_sbom, scan_manifest and check_affected and cve_remediation's for an installed version, is
113
+ the first version above the installed one that no affected range of the advisory holds: the
114
+ fixed bound of the range holding it, moved past any overlapping range of the same record.
115
+ CVE-2026-4800 for lodash 4.17.15 answered `fixed_in` 4.17.21, the bound of `[0, 4.17.21)`, while
116
+ `[4.0.0, 4.18.0)` on the same row still holds 4.17.21; it now answers 4.18.0, and after the #3075
117
+ re-import the row holds only `[4.0.0, 4.18.0)`, CVE-2026-4800's own range. When the bound moved, the API's
118
+ `fixed_in_detail` names the ranges it moved past. The API applies the rule; the server relays
119
+ it. cve_intel carries no `fixed_in`: its description, its `fixed_branches` schema description
120
+ and its note now state the same walk for `fixed_branches` (an affected version's fix is the
121
+ first `fixed` above it that no range of its row holds, none past a range without one), where
122
+ its description had said the `fixed` of the range holding the version is its fix.
123
+ - **A GitHub advisory's ranges go to the CVEs GitHub names for them** (#3075). Where GitHub's
124
+ attribution can be read (its own advisory record, then its one NVD advisory reference, then a
125
+ single CVE alias), a GHSA that OSV merged into a group of several CVEs no longer lends its ranges
126
+ to the others. A record none of these settles keeps its alias set, as before, so no CVE loses a
127
+ range it owns. Matches that came only from a range another CVE owned leave
128
+ check_sbom's, scan_manifest's and check_affected's answers.
129
+ - `cve_remediation` carries `fix_status`, the API's fix-status rule that CVE Pulse pages show
130
+ (`available`, `fix_referenced`, `fix_merged`, `none_confirmed` or `unknown`, with its `basis`),
131
+ and get_cve's `remediation` summary carries it first. Both relay it; the server derives nothing
132
+ (#2841). The rule reads the CVE record's own fix statement (#3076): a fix the record's solutions
133
+ state is `available` with `basis` `cna_solution` and `solution_by` (the sentence, `statement`,
134
+ and the CNA that wrote it, `cna`); a fix `patch_evidence` counts from the record with no sentence
135
+ to quote is `available` with `basis` `patch_evidence_statement` and `stated_in_record`; and
136
+ `unread_inputs` can name `cna_record`.
137
+ - `patch_evidence` has three more labels (#3076, #2518; core-backend): `vendor_advisory_fix` (a
138
+ vendor advisory listing a fix for the CVE under the vendor's own structured category: Red Hat's
139
+ CSAF `vendor_fix`, Microsoft's Vendor Fix), `cna_solution` (a sentence of the CVE record's
140
+ solutions that names a fixed version) and `description_fix` (a sentence of the CVE description
141
+ that does). get_cve's outputSchema names all eight.
142
+ - get_cve relays what the CVE record's CNA wrote (#3070, #3076; core-backend): `cna_solution`, the
143
+ sentence of its solutions that names the fixed release, verbatim; `date_public`, when the
144
+ vulnerability was made public as the CNA states it (`date_public_source` `cna`), which is not the
145
+ record's own `published`; and `cna` (`short_name`, `name`). The note names each as the CNA's
146
+ where the record carries it.
147
+ - get_cve's note gives `score_factors.bod_26_04` where the scorer wrote it (algorithm v3.4, #2880):
148
+ CISA BOD 26-04's remediation timeline for the CVE if the affected system is publicly exposed and
149
+ if it is not, with each input and its source. Its outputSchema describes
150
+ `echelongraph_ssvc_decision` and `score_factors`.
151
+ - CISA's required action is attributed to BOD 26-04, which replaced BOD 22-01 on 10 June 2026
152
+ (#3084): get_cve's note and its `kev_required_action` description, and cve_remediation's note and
153
+ description.
154
+ - `cve_remediation` takes the installed version: `ecosystem`, `package` and `version`, all three
155
+ or none, sent in the `X-EG-Ecosystem`, `X-EG-Package` and `X-EG-Version` request headers and
156
+ never in the URL. It then answers `installed` (that version's verdict against the CVE),
157
+ `fixed_in` and, when it is null, `fixed_in_reason`; that answer is private and `no-store`.
158
+ Fix tags on references match in either case (#2841, #3073).
159
+ - The `sbom_review` prompt reads a fixed version from `fixed_in` alone, and names the source
160
+ package where a row matched upstream (`matched_via` upstream, `matched_package`) (#2834, #2836).
161
+ - README, Claude Desktop: the "check that the file parses" step prints one command per OS with the
162
+ config path filled in (`"$HOME/Library/Application Support/…"` in Terminal,
163
+ `"$env:APPDATA\Claude\…"` in PowerShell). Through 2.7.2 it took a quoted `"<path to the file>"`,
164
+ and the macOS path pasted into it (`~/…`) is not expanded inside quotes, so a valid file answered
165
+ `ENOENT` (#3096).
166
+ - README: fits npm's 64 KiB with its Privacy and License sections; the long reference moved to
167
+ `docs/tools.md`, `docs/exposure-radar.md`, `docs/structured-results.md` and
168
+ `docs/rate-limits.md`, linked from it. Every count it states is held by a test (#3012, #2922).
169
+
170
+ - **Contract change: the structured result is held to 30,000 characters by default; pass
171
+ `full: true` for its data whole.** Claude Code hands the model `structuredContent`, not the text
172
+ blocks, and refuses one past `MAX_MCP_OUTPUT_TOKENS` (25,000 by default), saving it to a file:
173
+ `get_cve` for CVE-2021-44228 was 65,540 characters of `structuredContent` on production beside a
174
+ first text block of 23,759, so the model got a file path, not an answer. Through 2.7.x
175
+ `structuredContent.data` was always the answer whole. Now, when the whole structured result
176
+ (envelope, notes and data, as compact JSON) would pass 30,000 characters, its data is cut, in an
177
+ order each tool states. Where one row is large, every row is first thinned to some of its fields,
178
+ all rows kept, before any row leaves: check_sbom's and scan_manifest's rows to `index`, `purl`,
179
+ `verdict`, `not_assessed_reason`, `cve_ids`, `matched_package` and `matched_via`, each match to
180
+ `cve_id`, `kev_listed`, `ransomware`, `epss_score`, `effective_score`, `score_assessed`,
181
+ `fixed_in` and `fixed_in_reason`, every field the prompts read from a match (all 50 rows of the
182
+ Juice Shop 11.1.2 SBOM stay); check_affected's matches to their scores, flags, criteria and
183
+ `fixed_in`, then fewer; cve_remediation's advisories without each item's `product_ids`, then
184
+ without its text, the note then naming `get_vendor_advisory` as where each text is;
185
+ search_cves' rows to their description and scores, then the scores; the vendor-advisory tools' rows
186
+ without `affected_products`, then without `summary`. Then lists leave entries: get_cve's
187
+ `cpe_match` and `references` first; search_cves', kev_recent's, get_cwe's and the vendor-advisory
188
+ tools' rows from the end; check_affected's `excluded` and `undetermined` samples, then its
189
+ `matches`, `cve_ids` last; check_sbom's and scan_manifest's `not_sent_purls` and `not_checked`, then
190
+ their `results`, the `not_affected` rows leaving first, then the `not_assessed` ones, and only then
191
+ affected rows, from the end; cve_remediation's `vendor_remediations`, then `patches`;
192
+ exposure_radar's daily and weekly series, each keeping its newest entries, then its rankings,
193
+ then `kev_exposure.newest_kev`; then any other list at the top of data. The room left once a
194
+ list fits goes back to the lists emptied before it, as many of their entries as fit, their first
195
+ or a series' newest (scan_manifest's `not_checked` beside 64 thinned rows). No string is shortened and no value changes (a thinned row keeps
196
+ some of its fields, each optional in the outputSchema), so data still validates against the
197
+ outputSchema. Every success now carries `data_truncated` (false when data is whole), and, when it
198
+ is true, `truncated`: each list cut, by its path in data, with `total`, `returned` and, where its
199
+ entries are thinned, `thinned: true`; the note ends with sentences starting `DATA CUT` that say what
200
+ was cut and to which fields, how to read the rest (the offset, limit or page size, where the tool
201
+ pages; a page size said to fit data whole is counted on the rows whole) and that the same call
202
+ with `full: true` returns data whole. Every tool takes `full`
203
+ (boolean, optional). **A program that relied on data whole must pass `full: true`.** The first
204
+ text block is cut from the whole answer exactly as before (TEXT CUT); beside a cut data its TEXT CUT
205
+ sentence says the answer is whole in `structuredContent.data` with `full: true`, and so does each
206
+ sentence that points to another tool's data (#3013).
207
+ - check_sbom's and scan_manifest's first text block keeps each match's `fixed_in_reason` beside
208
+ `fixed_in` at every cut level that keeps matches. Their note, and the `sbom_review` and
209
+ `workload_triage` prompts, say `data.not_sent_purls` lists the purls not sent whole with
210
+ `full: true`, since a DATA CUT can cut that list; the position the note gives still rebuilds it.
211
+ scan_manifest's note says the same of `data.not_checked` (#3013).
212
+ - The `triage_cve` and `sbom_review` prompts read a vendor's workaround or mitigation step from
213
+ `get_vendor_advisory` where cve_remediation's note says its data or text leaves that item's text
214
+ out or shortens it (#3013).
215
+ - scan_manifest answers a file of very many entries: it read each file's entries into its lists as
216
+ the arguments of one call, and a `requirements.txt` of some 170,000 unpinned lines (4.9 million
217
+ characters) failed with "Maximum call stack size exceeded" (#3013).
218
+ - The `cve://` resource reads get_cve's record whole (`full: true`) and cuts it as before; its
219
+ DATA CUT note says get_cve's `structuredContent.data` is whole with `full: true`, and, where it
220
+ cuts, its envelope says `data_truncated: true` with `truncated` naming each list it cut (#3013).
221
+ - README: Claude Code's setup names `MAX_MCP_OUTPUT_TOKENS`; "Long answers" says which part of a
222
+ result each kind of client reads and how each is cut; the Structured results table gains
223
+ `data_truncated` and `truncated` (#3013).
224
+ - `test/text-bound.mjs` records each production-shaped case's structured size beside its text
225
+ size, `text-bound-live.mjs` measures and records it, and the suite fails when any default answer's
226
+ structured result is past the budget (#3013).
227
+
9
228
  ## 2.7.2 — 2026-10-11
10
229
 
11
230
  - exposure_radar flags every radar whose last completed check is past its threshold, not only the