echelongraph-mcp 2.6.3 → 2.6.6

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,303 @@ 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.6 — 2026-10-05
10
+
11
+ - Correction to 2.6.5 (#2775). Its entry says: "On the hosted endpoint it is the client going away
12
+ (its own timeout, a closed tab): every API call of that request is cancelled". That was not true
13
+ of the hosted endpoint. Cloud Run does not pass a client's disconnect on to a container it
14
+ speaks HTTP/1.1 to ("When you use HTTP/1.1 on Cloud Run, client disconnect events are not
15
+ propagated to the Cloud Run container",
16
+ <https://docs.cloud.google.com/run/docs/troubleshooting>). In production on 2026-10-05, four
17
+ `check_sbom` calls had their client abort after 0.4 s or 1.5 s. Each was logged `status` 200
18
+ with `client_closed` false and ran to its end, 2.0 to 5.3 s. Together they held both large-body
19
+ slots, so a large body sent 1 s later was answered 503. The same page says HTTP/2 does pass the
20
+ disconnect on. So `dist/http.js` now serves HTTP/2 cleartext (h2c) when `MCP_H2C=1`, and the
21
+ hosted deployment sets it together with Cloud Run's `--use-http2`. With that flag, Cloud Run
22
+ speaks only h2c to the container. The server acts on a reset stream the moment it arrives, with
23
+ any error code including `NO_ERROR`: the API calls are cancelled, `check_sbom` sends no further
24
+ batch and ends a `Retry-After` wait, the large-body slot is freed, and the `mcp_request` line
25
+ says `status` 499 with `client_closed` true. What Cloud Run forwards is only partly measured.
26
+ On test deployments with `--use-http2` on 2026-10-05, ordinary calls, sessions and `/health`
27
+ were all answered over h2c. A client that stopped reading a streamed answer had its stream reset
28
+ by Cloud Run 15 s in (`rst_code` 2), and that was logged 499. A client that aborts mid-call
29
+ before any answer was not observed, because every probe call was answered first, so whether
30
+ Cloud Run forwards that reset at once is not yet shown. Until it is, an abandoned call may still
31
+ run to its end, a `check_sbom` call for at most its 50-second budget, holding its slot. A new line, `mcp_client_gone`,
32
+ records when the server learned the client went: `after_ms` from the request's start, whether
33
+ the answer had started (`answer_started`), and an HTTP/2 stream's `rst_code`. It holds nothing
34
+ of the request. Run yourself, `dist/http.js` speaks HTTP/1.1 as before. With `MCP_H2C=1` it
35
+ speaks h2c with prior knowledge only, and an HTTP/1.1 request gets no answer.
36
+ - README, "Privacy: what is sent where": the hosted endpoint's paragraph now also says what the API logs
37
+ of the calls it makes for you (#2797): each under your address, with its URL path (for a CVE, CWE or
38
+ advisory lookup, the ID), and not its query string, the header-carried terms or which purls. The
39
+ privacy policy's new Section 11, https://echelongraph.io/privacy#hosted-mcp-endpoint , says the same.
40
+ - `exposure_radar` relays and labels `mcp_servers.withheld_opted_out` (#2822): how many hostnames
41
+ whose owner opted out of scanning the API left out of every other `mcp_servers` number. Such a
42
+ hostname is not checked again while the opt-out stands, so its last verdict cannot be re-checked;
43
+ the API now withholds it from the MCP-server counts as its unparameterised AI-exposure answer
44
+ already did. An answer without the field, from an API that predates it, counted those hostnames,
45
+ and the note says so instead of reading the absence as 0; a value that is not a whole number is
46
+ left out and named.
47
+ - Claude Desktop in one click (#2815). From this release on, every GitHub release of
48
+ [github.com/echelongraph/echelongraph-mcp](https://github.com/echelongraph/echelongraph-mcp/releases)
49
+ carries `echelongraph-mcp.mcpb`, a Claude Desktop extension: open it and choose Install. It needs
50
+ no Node.js on your machine, since Claude Desktop runs it with the Node.js it ships, and no JSON to edit.
51
+ The README's Claude Desktop section links it as "Install in Claude Desktop", ahead of the custom
52
+ connector and the `npx` setup. The release workflow builds it from the release tag, the same tag
53
+ the npm package is published from: `listings/mcpb/build.sh` validates the manifest with the pinned
54
+ MCPB CLI and packs. Then, on a separate read-only runner that never ran the MCPB CLI,
55
+ `listings/mcpb/check-bundle.mjs` refuses a bundle that holds a top-level file `build.sh` does not
56
+ stage, a symlink, or a file where `build.sh` stages a directory; that lacks the icon or entry point
57
+ its manifest names, or `node_modules/`; whose `package.json`, `LICENSE`, `README.md` or `dist/` is not the tested npm
58
+ tarball's, byte for byte (a file changed, missing or added); whose `node_modules/` adds or changes
59
+ a file a production install from the lockfile does not have; that is at another version; or whose
60
+ own server, started with `PATH`, `HOME` and `ECHELONGRAPH_API_BASE` only (none of the caller's
61
+ `NODE_PATH`, `NODE_OPTIONS` or other variables), does not list the
62
+ manifest's 14 tools with their titles. npm publishes only after that check passes, so a manifest
63
+ or bundle defect stops the release before npm has the version. A separate job, which runs no
64
+ dependency code, signs the checked bundle with a SLSA build-provenance attestation (Sigstore,
65
+ keyless), attaches it and its Sigstore bundle to the release, each on its own, and checks the
66
+ file the release serves with `gh attestation verify`. To check a download yourself:
67
+ `gh attestation verify echelongraph-mcp.mcpb --repo echelongraph/echelongraph-mcp`.
68
+ Releases up to 2.6.5 carry no bundle.
69
+ - Repository only, not in the package: `listings/mcpb/check-bundle.mjs` and
70
+ `test/mcpb-release.test.mjs`; `test/mcp-stdio-client.mjs` takes a `cwd`.
71
+ - `sbom_review` reads the advisory interval in `check_sbom`'s `match_reason` by its closing
72
+ bracket, without the hedge of 2.6.5 and earlier. `[A, B)` is a fixed bound: B, the first version the advisory
73
+ records as not affected, is given as the fixed version (`[A, ∞)` names none). `[A, B] (B is the
74
+ last affected version, not a fix)` is an OSV `last_affected` bound: B is still affected and the
75
+ range records no fix, so neither B nor any version at or below it is given as a fixed version.
76
+ The bracket alone decides: a cut text clips `match_reason` to 200 characters, which can drop the
77
+ words after it for a long package name, and where it is clipped before the closing bracket
78
+ (`[A, B…`), no fixed version is taken from that `match_reason`.
79
+ Through 2.6.5 core-backend rendered both bounds as `[A, B)`, so the prompt gave B only as "the
80
+ end of that interval"; core-backend now renders a `last_affected` bound closed (#2830). This
81
+ wording assumes the API serves the new rendering: it is released after core-backend is deployed.
82
+ - `cve_intel` relays `fixed_branches` on each `affected_packages` row: every affected range of the
83
+ package on record, each with `introduced` and `fixed` (the range's fix) or, where no fix is on
84
+ record, `last_affected` (never in a field named fixed), and with `advisory_id` (the OSV record that
85
+ published the range) and `source` (the loader's label for that record), from core-backend's
86
+ `/api/v1/public/cves/:id/enrichment` since #2817. Ranges that name their record are in the order
87
+ the record lists them; ranges loaded before the record was stored (both null) are oldest first.
88
+ `fixed_branches` is null where a package's ranges were never loaded, and `[]` where no version
89
+ range is on record (none in the advisory, only commit ranges, or more than are stored), which is
90
+ not a finding that no fix exists. `fixed_version` is one range's fix per package,
91
+ kept for compatibility: for CVE-2021-44228 production served log4j-core 2.12.2 alone (2026-10-04),
92
+ the fix for 2.4 to 2.12.1, older than an affected 2.14.1, while the advisory
93
+ (GHSA-jfh8-c2jp-5v3q) gives three ranges, fixed in 2.3.1, 2.12.2 and 2.15.0. The description says
94
+ how to pick the range for an installed version, and the note counts the rows with more than one
95
+ range, and those with no range on record (`fixed_branches` null, or `[]` beside a `fixed_version`),
96
+ where `fixed_version` is all the answer holds. An API older than #2817 is said to be one (#2817).
97
+ - `triage_cve`'s patch line gives each affected range with its fix from `fixed_branches`, and
98
+ `fixed_version` only for a package without them; `sbom_review` gives a component the fix of the
99
+ `fixed_branches` range that holds its installed version (log4j-core 2.14.1: 2.15.0), after the
100
+ advisory interval in `check_sbom`'s `match_reason`, and `fixed_version` only for a package without
101
+ `fixed_branches` (#2817).
102
+ - `vendor_advisories_for_cve`, `search_vendor_advisories` and `get_vendor_advisory` relay each
103
+ vendor's `held_since`, which the coverage API now serves: the start of the window EchelonGraph has
104
+ read the vendor's advisories from, before which an advisory may not be held.
105
+ `vendors_not_fully_held` reads it, not the earliest held advisory. For a vendor with no history read
106
+ the earliest held advisory is not where its window begins: Cisco's poller first read the advisories
107
+ Cisco had changed in the last 90 days, Red Hat's and GitHub's the last 7, so an older advisory the
108
+ vendor revised later is held while the ones published beside it are not. Cisco's earliest held
109
+ advisory is dated 2024-10-23 and Red Hat's 2009-02-04, so CVE-2025-20188 left out Cisco and
110
+ CVE-2014-0160 left out Red Hat, and their empty answers read as the vendor having published nothing;
111
+ both are listed now. GitHub is now listed too for any CVE ID year that begins before the start of
112
+ its first poll's 7-day window (`held_since`), not only for years up to that of its earliest held
113
+ advisory (2017). Against an API without `held_since`, a vendor is taken as held from its earliest
114
+ advisory only when its history read is complete, and otherwise listed, its window written "not known
115
+ to be without a gap" (#2803).
116
+
117
+ ## 2.6.5 — 2026-10-05
118
+
119
+ 2.6.4 was tagged and never published: its Release run failed `test/http-memory.test.mjs` on Node 24,
120
+ where `--max-old-space-size=128` alone allowed a 320 MiB heap in a 256 MiB instance. 2.6.5 is 2.6.4
121
+ plus the fix below; the `v2.6.4` tag stays, unpublished, because a tag is never moved.
122
+
123
+ - Hosted endpoint only: `Dockerfile.http` also passes `--max-semi-space-size=16`, so V8's heap limit is
124
+ 176 MiB on Node 22 (unchanged: the production image) and on Node 24 (was 320 MiB). The memory cap of
125
+ #2773 now holds whichever Node the image uses.
126
+
127
+ - README: the header links the directories that list this server, Smithery and Glama (#2723). Smithery's
128
+ verification looks for that link; `test/listings.test.mjs` pins it.
129
+
130
+ - Hosted endpoint only: an instance serves at most 64 requests at once (`MCP_MAX_IN_FLIGHT`); the
131
+ next, its body read first (so a slow upload holds no place), waits up to 10 seconds
132
+ (`MCP_ADMISSION_WAIT_MS`) for one to finish, and is then answered HTTP 503 with `Retry-After` and
133
+ JSON-RPC error `-32030`. A 2026-07-28 `subscriptions/listen` stream is not counted: it stays open
134
+ until its client leaves, carrying only keepalives (the server announces no list changes), so 64
135
+ idle ones would leave every other request waiting 10 s for a 503; the SDK refuses a stream past
136
+ 1,024 open per process. The image runs Node with V8's heap capped at 128 MiB, half the
137
+ instance's memory. Measured in a 256 MiB cgroup on a 16 GB host: two maximum-size SBOM documents
138
+ beside 125 or more other requests got the server killed with every request on it, and holding
139
+ that took both bounds. Node sized V8's heap from the host's memory, so garbage piled up until
140
+ the kernel killed the process (whether Cloud Run's instance reports more than its 256 MiB is not
141
+ verified), and 250 requests live at once exhausted even the capped heap. With both, 250 sent as
142
+ they come are all served (the last after waiting about 9 s), peaking at 82-84% of the memory,
143
+ 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
144
+ `queued_ms`, the start-up line `heap_limit_mb` and `memory_limit_mb`, and a process started
145
+ without the cap logs `mcp_remote_heap_unbounded`, where Node can read the container's limit
146
+ (`memory_limit_mb` not null) (#2773).
147
+ - `check_sbom` stops when its call is cancelled: no further batch is sent, the batch in flight is
148
+ cut off, and a `Retry-After` wait ends. Over stdio that is the client's
149
+ `notifications/cancelled`, which before left the call sending its batches to the end. On the
150
+ hosted endpoint it is the client going away (its own timeout, a closed tab): every API call of
151
+ that request is cancelled, a `check_sbom` call holds its large-body slot until it has stopped,
152
+ not only until its client has gone, and the `mcp_request` line says `status` 499 with
153
+ `client_closed` true. Before, an abandoned call ran its batches to the end, spending the
154
+ client's API budget, and was logged as 200 (#2775).
155
+ - Repository only, not in the package: the production synthetic's hosted leg sends `check_sbom`
156
+ its 201 purls as a CycloneDX document, a ~138,000-byte request body, twice the endpoint's
157
+ 64 KiB general cap, and fails the probe unless the answer is `measured` from the document; a
158
+ return of the 413 that #2747 removed now fails it (`rpc_error`, `-32600`). The synthetic's alert
159
+ runbook says how to read that failure, and a memory alert on the hosted endpoint is written
160
+ beside its uptime check (#2774, #2773).
161
+ - The prompts ask only for what their tools' first text block keeps, at their largest input. In
162
+ 2.6.3 each of the four asked for something that text leaves out of a large answer, or that the
163
+ call it made did not return, so a client that passes only `content` to the model never showed
164
+ it (#2799):
165
+ - `kev_weekly_brief` reads `kev_recent` 50 rows a page, not 200. Each line of the brief names
166
+ `kev_vuln_name` and `kev_due_date`. Read through the hosted endpoint on 2026-10-04 (2.6.3),
167
+ 365 days at limit 200 (306 CVEs) kept 5 fields a row in the text, neither of those two among
168
+ them. A page of 50 comes back whole: 27,514 characters for production's 50 newest.
169
+ - `check_affected` keeps `ransomware` and `epss_score` on every match at every cut level, the
170
+ two fields `am_i_affected` lists for each match. In 2.6.3 a list at the API's cap of 200
171
+ matches kept `cve_id`, `kev_listed`, `effective_score`, `effective_severity` and
172
+ `score_assessed`: hosted, `linux_kernel` 5.10.0 had 20 CISA-KEV-listed matches and 1 with
173
+ known ransomware use, named nowhere in the text. Since 200 such matches with
174
+ `effective_severity` as well come to 33,240 characters, past the budget on their own, the
175
+ leanest level leaves `effective_severity` out. The `excluded` and `undetermined` samples now
176
+ keep their first 10 entries in the text (`excluded_count` and `undetermined_count` count every
177
+ entry), so every match still stays in the text. The description's "at least" list says so.
178
+ - `sbom_review` sends the purls not sent again from `data.not_sent_purls` only where the text
179
+ holds that list whole. Where the note says the text cuts it (to its first 10), the model
180
+ rebuilds the purls from the document instead: its distinct purls from the position the note
181
+ gives, in the order the note gives. `check_sbom`'s note now gives that position, and the order
182
+ it counts the input's purls in (a CycloneDX document's components depth first, each one's
183
+ nested components right after it, each purl at its first place), whenever purls are not sent,
184
+ not only when the text cuts the list. The prompt also says what to do when the note says rows
185
+ are left out of the text: list the rows the text carries, and give the rest as
186
+ `data.summary`'s counts.
187
+ - `sbom_review` reads each affected CVE's fixed versions from `cve_intel`, called beside
188
+ `get_cve` (`fixed_version` of its `affected_packages` and `fixed_versions` rows for the
189
+ component's package). In 2.6.3 it asked for "fixed versions where a result gives them", and
190
+ `check_sbom`'s text keeps `match_reason`, the advisory interval that names the fixed version,
191
+ only at its first cut level: the 50 components of a Juice Shop SBOM are cut past it.
192
+ - `sbom_review` never gives a fixed version that is not strictly greater than the component's
193
+ installed version, compared in the ecosystem's version order (where the model is not sure of
194
+ the order, it says so and gives none). It takes the end of the advisory interval in
195
+ `match_reason` first, where the text keeps it, and otherwise `cve_intel`'s `fixed_version`,
196
+ said to be one value per package, the advisory's last range's, which need not be the fix on
197
+ the component's branch (#2817). Where no version qualifies, the line says the results give no
198
+ fixed version for the component's branch and points to the CVE's advisory (`get_cve`'s
199
+ references). Read through the hosted endpoint on 2026-10-04 (2.6.3), `cve_intel` gives
200
+ log4j-core 2.12.2 for CVE-2021-44228, and `check_sbom`'s matches for 2.14.1 say it "is listed
201
+ verbatim in the advisory's affected versions", so the entry above alone had the model give
202
+ 2.12.2 as the fix for 2.14.1. The prompt also reads each component's ecosystem, package and version
203
+ from its purl, which every row the text keeps carries; the leanest cut, reached at 2,000 purls,
204
+ leaves the row's own fields out.
205
+ It maps a purl as `check_sbom`'s matcher does: the sixteen purl types with an ecosystem (`hex`,
206
+ `pub`, `swift`, `hackage`, `cran`, `bitnami`, `conan` and `githubact` among them, which the
207
+ first wording left out), `swift` and `githubact` named with their namespace as `npm`, `golang`
208
+ and `composer` are, and `deb`, `apk`, `alpine` and `rpm` by a `distro` qualifier of
209
+ `debian-N` (its major release), `ubuntu-V` or `alpine-X.Y` (cut to major.minor); for any other
210
+ the ecosystem is unknown, and the fix list gives no version for it.
211
+ - `triage_cve`'s patch line says that `cve_intel`'s `fixed_version` is one value per package, the
212
+ advisory's last range's, and is given as what `cve_intel` records for that package, never as
213
+ the fix for every affected branch; for a branch's fix it points to `get_cve`'s references
214
+ tagged Vendor Advisory or Patch and to the vendor advisories (#2817). The line had asked for
215
+ "fixed versions" alone, with no installed version to hold them against, so for CVE-2021-44228
216
+ it could give log4j-core 2.12.2, `cve_intel`'s one value on 2026-10-04, as the patch, below the
217
+ affected 2.14.1.
218
+ - `triage_cve` reads CISA's due date, `kev_due_date`, from `get_cve`'s record and no longer calls
219
+ `kev_recent`. Its step 6 read it from a `kev_recent` page of the CVE's `kev_added_date`, with no
220
+ limit and no paging. Read through the hosted endpoint on 2026-10-04 (2.6.3), the page for
221
+ 2021-11-03 held 50 of the 287 CVEs CISA added that day, and not CVE-2021-40444, one of them;
222
+ `get_cve` for CVE-2021-40444 carried its `kev_due_date` in its first text block.
223
+ - `cve://{cve_id}` stays under the 60,000-character ceiling. Its `data` is cut as `get_cve`'s
224
+ first text block is, laid out the same way, after the envelope. In 2.6.3 it was `get_cve`'s
225
+ structured result pretty-printed whole: 86,559 characters for CVE-2021-44228 on the hosted
226
+ endpoint, against 23,705 now on the suite's production-shaped answer. Its notes no longer carry
227
+ `get_cve`'s `TEXT CUT` sentences, which describe a first text block the resource does not have
228
+ (they said `cpe_match` kept 64 entries while the resource carried all 396). When `data` is cut,
229
+ the notes end with the resource's own sentence instead, starting `DATA CUT`, which names
230
+ `get_cve`'s `structuredContent.data` as the record whole. A record that fits is unchanged,
231
+ character for character. The resource's description and the methodology say so (#2801).
232
+ - No note promises a record or a component's matches "whole" from a tool whose text is cut too.
233
+ The `TEXT CUT` sentences of `search_cves`, `kev_recent`, `get_cwe` and `check_affected` say that
234
+ `get_cve` returns a record whole in its `structuredContent.data` and, in its first text block,
235
+ with every field, each list cut to its first entries past 30,000 characters. `check_sbom`'s
236
+ sentence says the same of `check_affected` and a component's matches, and the vendor-advisory
237
+ tools' of `get_vendor_advisory`. `get_cve`'s description and the README begin "One CVE's
238
+ record", no longer "Full record for one CVE". The source comments and the README no longer say
239
+ that `content[0]` always equals `data`. The 2.6.3 entry below gives the sizes production's record
240
+ holds for that release (26,011, 27,777, 28,793, 31,583 and 23,028), measured after #2729, in
241
+ place of the 25,951, 27,717, 28,733, 31,588 and 19,917 measured before it (#2802).
242
+ - Repository only, not in the package (#2800, #2799):
243
+ - Every production-shaped case's text is held within 50 characters of its recorded size, in
244
+ either direction. A text that shrinks is re-measured as one that grows is.
245
+ - The fields each description says a cut row keeps "at least" are read from the description
246
+ and checked on every row of every production-shaped case, for `search_cves`, `get_cwe`,
247
+ `kev_recent`, `check_affected` (its samples too), `check_sbom` and the vendor-advisory lists,
248
+ and at least one case of each tool must be cut. No answer the API serves reaches the cut of
249
+ `get_cwe` (each row's description is cut to 240 characters at the source) or of
250
+ `vendor_advisories_for_cve` (at most 20 short rows), so each has a forced case: production's
251
+ rows made longer. `get_cve` is held to keep every field. Removing `severity`, `cvss_v3_score`,
252
+ `echelongraph_score`, `score_assessed`, `epss_score` and `kev_listed` from `search_cves`'s
253
+ cut levels, which left 2.6.3's suite green, now fails it, as does dropping a field from
254
+ `get_cwe`'s.
255
+ - Each prompt's tool calls are made as the prompt says, at its largest input, on
256
+ production-shaped answers, and every field the prompt names must be in the text a client
257
+ that passes only `content` shows the model. Every tool a prompt names must be one of those
258
+ calls.
259
+ - A new test makes each prompt's tool calls as the prompt says, at its largest input, on
260
+ production-shaped answers, and checks every field the prompt names in the text: 365 days of
261
+ KEV additions, `check_affected` at its cap of 200 matches (both lookup paths), a 2,000-purl
262
+ SBOM answered in full and rate-limited, and `triage_cve`'s six tools.
263
+ - That test reads `sbom_review`'s `ecosystem`, `package` and `version` as fields: on a row whose
264
+ text leaves them out, reading them from the purl as the prompt says must give the answer's.
265
+ On production's `cve_intel` for CVE-2021-44228 and production's `check_sbom` answer for
266
+ log4j-core 2.14.1 (captured 2026-10-04), the prompt's fix rule must offer no fixed version,
267
+ 2.12.2 among the candidates, and on Juice Shop's lodash 4.17.19 it must offer 4.17.21 (#2817).
268
+ - `cve_summary` no longer fails when the `poller` block changes JSON type: a poller field the
269
+ answer sends in a type other than the one its outputSchema describes (`interval` as a Go
270
+ duration such as 1200000000000, `last_poll_at` as unix seconds, `cves_ingested` as a string) is
271
+ left out of `data` and named in the note, and a `poller` that is neither a JSON object nor null
272
+ is left out whole. `summary` and the rest of the answer are relayed as sent, measured. From
273
+ 2.6.2 one such field failed the whole call with `unexpected_shape` and withheld `summary.total`,
274
+ and an installed 2.6.2 or 2.6.3 still does: upgrade. The tool's description, the
275
+ `echelongraph://methodology` resource and the README say that `cve_summary`'s `data` is the
276
+ API's JSON less what the note names as left out (#2771).
277
+ - `echelongraph://sources` says what the NVD poller's `poll_count` and `poll_errors` are whenever
278
+ it relays either: that one instance's counters since it last started, zeroed on every restart,
279
+ never the feed's reliability. The resource's description and the README name all four fields it
280
+ relays. A `poller` the answer sends as something other than a JSON object is named as such,
281
+ never as a missing block (#2770).
282
+ - package.json's `description` fits the 255 characters npm's registry serves, so npm search,
283
+ `npm view` and the package page show it whole, with the per-CVE exposure footprint, its Shodan
284
+ attribution and Shodan's ownership sentence; through 2.6.3 npm cut the 443-character text
285
+ mid-phrase. A test holds it to 255 characters and whole sentences (#2779).
286
+ - package.json's `keywords`, and the public repository's GitHub topics, are held to the
287
+ description's wording rules by a test: no claim the package's own copy is barred from, no
288
+ ip:port service count named as machines, and nothing exclusive or comparative (#2780).
289
+ - Tests: check_sbom's description and the README's privacy section are pinned to saying that over
290
+ the hosted endpoint the document is the request body and reaches EchelonGraph's server, which
291
+ neither logs nor keeps it, and to never denying that the document is sent (#2796). The denials
292
+ are one list, `test/document-denials.mjs`, that the monorepo's /pulse/mcp check reads too, in
293
+ every noun `check_sbom` uses for its input; the README check's own list had passed "The server
294
+ never sends the document.", "The document is processed locally." and "Only the purls are sent."
295
+ (#2796, #2795).
296
+ - `check_sbom`'s `sbom` argument said "its purls are read here and only they are sent", which over
297
+ the hosted endpoint is not so: there the whole document is the request body. It now says the
298
+ purls are read by this MCP server and only they are sent to the API, and that over the hosted
299
+ endpoint (mcp.echelongraph.io) the document is the request body, in the description's own words.
300
+ The README Tools table's `check_sbom` row, which ended "the document is not.", says the same.
301
+ Tests: every string of `check_sbom`'s tools/list entry, its argument and output descriptions
302
+ among them, and that README row are now held to the denial list, which also reads "not sent on"
303
+ as true only when the clause ends there, and catches more wordings of where the document stays
304
+ or is processed (#2796).
305
+
9
306
  ## 2.6.3 — 2026-10-04
10
307
 
11
308
  - `exposure_radar` relays the observation window each count was measured over, where the API now
@@ -50,8 +347,8 @@ from the commit tagged `v<version>`.
50
347
  was 133,300 characters of text, `get_cve` CVE-2021-44228 80,897, `check_affected` openssl 3.0.0
51
348
  156,058 (the Linux kernel at 5.10.0, 364,408), `check_sbom` on a 50-component SBOM 112,820,
52
349
  `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
350
+ the 60,000-character ceiling; the same calls now measure 13,513, 22,960, 26,011 (27,777),
351
+ 28,793, 31,583 and 23,028. The JSON is first laid out one row a line, which loses nothing; past
55
352
  that, a list tool cuts each row to the fields it names (the description cut to 200 characters,
56
353
  every long string ending in "…"), then to fewer, and leaves rows out only when its leanest cut
57
354
  is still too long (`check_sbom` leaves out `not_affected` rows first); `get_cve` keeps the first