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 +219 -0
- package/README.md +163 -433
- package/dist/fixStatus.js +30 -0
- package/dist/httpPolicy.js +16 -1
- package/dist/index.js +287 -34
- package/dist/prompts.js +63 -33
- package/dist/requiredText.js +16 -1
- package/dist/resources.js +32 -6
- package/dist/textBudget.js +588 -22
- package/dist/tools/check_affected.js +45 -12
- package/dist/tools/check_sbom.js +158 -9
- package/dist/tools/cve_intel.js +63 -9
- package/dist/tools/cve_remediation.js +230 -16
- package/dist/tools/epss_history.js +10 -4
- package/dist/tools/get_cwe.js +7 -2
- package/dist/tools/kev_recent.js +22 -3
- package/dist/tools/match_batch.js +27 -7
- package/dist/tools/scan_manifest.js +35 -10
- package/dist/tools/vendor_advisories.js +47 -11
- package/docs/exposure-radar.md +181 -0
- package/docs/rate-limits.md +50 -0
- package/docs/structured-results.md +43 -0
- package/docs/tools.md +167 -0
- package/package.json +2 -1
- package/server.json +2 -2
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
|