echelongraph-mcp 3.0.0 → 3.1.0

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/docs/tools.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  Part of the [README](../README.md), whose Tools table says what each tool answers. This page says,
4
4
  field by field, how `cve_summary`, `get_cve`, `search_cves`, `cve_exposure` and `check_affected`
5
- read the API's answer; `exposure_radar` has [a page of its own](exposure-radar.md). It is kept out
5
+ read the API's answer, and how `check_sbom` and `scan_manifest` cut their rows past the
6
+ structured result's budget; `exposure_radar` has [a page of its own](exposure-radar.md). It is kept out
6
7
  of the README because npm keeps only the first 65,536 characters of a package's README, and the
7
8
  package ships it in `docs/`. Exposure counts are derived from Shodan data. Shodan data is owned by
8
9
  Shodan, which holds its copyright (© Shodan).
@@ -34,6 +35,15 @@ outputSchema describes is left out and named in the note, and a `poller` that is
34
35
  object nor null is left out whole; `summary` is relayed either way, so a change to that diagnostic
35
36
  block never withholds the counts.
36
37
 
38
+ `nvd_poll` is when the fleet of API instances last completed an NVD poll: `last_completed_at` (the
39
+ end of the last window fetched and stored without an error), with `scope` `fleet` and the poll's
40
+ `interval`. The API leaves it out when that time is unknown, and never puts the answering
41
+ instance's counters in its place. It is the result's `freshness` (`freshness.nvd_poll`, its time as
42
+ `last_run_at`) where `last_completed_at` is a real RFC 3339 time at most 5 minutes after this
43
+ server's clock and `scope` is `fleet`. Otherwise `freshness` is `null`, and the note says whether
44
+ the answer carries no `nvd_poll` or one without such a time. `poller.last_poll_at` is never the
45
+ feed's freshness.
46
+
37
47
  The tool relays the API's JSON as it was sent, less a `poller` field left out as above. When
38
48
  `summary.none`, `summary.nvd_none` or `summary.rejected` is above zero, the note says what it
39
49
  counts, with the number from the answer.
@@ -62,8 +72,8 @@ Both tools relay the API's JSON as it was sent, and the note says what the score
62
72
 
63
73
  ## What `patch_available` says
64
74
 
65
- `patch_available` is `true` when EchelonGraph holds evidence of a fix for the CVE, and
66
- `patch_evidence` lists the sources of that evidence, strongest first: `vendor_patch` (a vendor
75
+ `patch_available` is `true` when EchelonGraph holds, or once held, evidence of a fix for the CVE,
76
+ and `patch_evidence` lists the sources it holds now, strongest first: `vendor_patch` (a vendor
67
77
  patch on record, from Ubuntu or Red Hat), `fixed_version` (a fixed version recorded for an
68
78
  affected package, or the fixed bound of an affected version range), `release_reference` (a
69
79
  reference that EchelonGraph confirmed through the GitHub API is a published release containing
@@ -74,11 +84,15 @@ CSAF `vendor_fix`, Microsoft's Vendor Fix), `nvd_patch_reference` (an NVD refere
74
84
  (a sentence of the CVE record's solutions, written by its CNA, that names a fixed version) and
75
85
  `description_fix` (a sentence of the CVE description that names a fixed version). A reference page
76
86
  that only mentions a patch, and a CVE's presence in CISA's KEV catalog, are not evidence.
77
- `get_cve` relays the CNA's own sentence as `cna_solution`, its date for when the vulnerability was
78
- made public as `date_public` (with `date_public_source` `cna`), and the CNA as `cna`, each only
79
- where the CVE record has it. `false` means no fix evidence is on record, which is **not** a
87
+ `get_cve` relays the CNA's own sentence as `cna_solution`, which section of the record it is as
88
+ `cna_solution_section` (`solution` or `workaround`: a workaround's sentence is the record's
89
+ workaround, never its solution), its date for when the vulnerability was made public as
90
+ `date_public` (with `date_public_source` `cna`), and the CNA as `cna`, each only where the CVE
91
+ record has it. `false` means no fix evidence is on record, which is **not** a
80
92
  finding that no fix exists. Once `true`, `patch_available` is not set back to `false`, so it can be
81
- `true` beside an empty `patch_evidence`. An answer with no `patch_evidence` (an API older than that
93
+ `true` beside an empty `patch_evidence`: evidence of a fix once held has since been withdrawn, and
94
+ `remediation.fix_status` in the CVE's record is the current reading, not `patch_available` (since
95
+ 3.0.2 the field's outputSchema text says so). An answer with no `patch_evidence` (an API older than that
82
96
  field) does not name the evidence, and a `false` there is not a finding that no fix exists either.
83
97
  Both tools relay the two fields as the API sends them.
84
98
 
@@ -129,9 +143,14 @@ affected, by one of two lookup paths:
129
143
  criteria name that product with a version range that includes the version. It matches the
130
144
  token across vendors, so each match carries `cpe_vendor` and `vendor_unknown`: a match with
131
145
  `vendor_unknown` true is real, but its vendor is not verified to be yours.
132
- - **Registry path** — `ecosystem` (`npm`, `PyPI`, `Maven`, …), `package` and `version`. Each OSV
133
- advisory record EchelonGraph holds for the package is decided against the version as
134
- affected (a match), not affected (`not_affected_count`) or undetermined.
146
+ - **Registry path** — `ecosystem` (`npm`, `PyPI`, `Maven`, …), `package` and `version`. Each CVE
147
+ named by the OSV advisory records EchelonGraph holds for the package is decided against the
148
+ version once, as affected (one match), not affected (`not_affected_count`) or undetermined
149
+ (`undetermined_count`). When OSV publishes one CVE for the package in several records (a GHSA
150
+ and a package repository's own advisory, say), EchelonGraph stores a row for each and decides
151
+ them together, so the two counts count CVEs, while `advisories_considered` counts the stored
152
+ rows: a CVE stored as two rows and cleared counts 2 in `advisories_considered` and 1 in
153
+ `not_affected_count`.
135
154
 
136
155
  Read `assessed` before `count`. `assessed` false means the lookup did not evaluate the
137
156
  component, and `not_assessed_reason` says why: `product_not_in_cpe_corpus`,
@@ -152,8 +171,19 @@ holding interval's fixed bound, moved past any overlapping interval that still h
152
171
  and `fixed_in_detail` then names the intervals it moved past (#3074). `fixed_in` is always above
153
172
  the version asked about. It is null where no such version is recorded (the walk reaches an
154
173
  interval that ends at `last_affected`, or has no end, with none beyond it) or no interval holds
155
- the version, and `fixed_in_reason` then says which: a null is not a finding that no fix exists. A CPE
156
- match carries no `fixed_in`, since the end of a CPE version range is not a fix. `check_sbom` and
174
+ the version, and `fixed_in_reason` then says which: a null is not a finding that no fix exists. When
175
+ a CVE is stored as several records (#3137), `fixed_in` is walked over the records that store
176
+ intervals. A record of the same CVE that stores none withholds it only when that record's versions
177
+ or recorded fix may hold it affected. A record that cannot decide it (no version data, a git commit
178
+ range, or one recorded fix that `fixed_in` is at or past) does not, so the same lookup at
179
+ `fixed_in` itself can come back undetermined rather than not affected. A CPE
180
+ match carries no `fixed_in`, since the end of a CPE version range is not a fix. Once
181
+ EchelonGraph's API serves it, a CPE match carries that end in its own field, as NVD states it,
182
+ `cpe_version_end_excluding` (the first version the matched range no longer holds) or
183
+ `cpe_version_end_including` (the last one it holds), absent where the range sets no end. Only the
184
+ first cut level of the text and the first thinned level of `data` keep them, so a CPE answer cut
185
+ past those levels, as typical ones are, carries them only with `full: true`. Another range of the
186
+ same CVE, or a vendor backport, can sit past it, so it is never given as a fixed version. `check_sbom` and
157
187
  `scan_manifest` relay the same fields (`interval`, `fixed_in`, `fixed_in_reason` and
158
188
  `fixed_in_detail`) on each match.
159
189
 
@@ -161,3 +191,110 @@ match carries no `fixed_in`, since the end of a CPE version range is not a fix.
161
191
  `X-EG-Ecosystem` and `X-EG-Package` request headers, never in the URL, so request logs and
162
192
  trace spans that record URLs do not hold what was looked up. The endpoint reads a CPE vendor
163
193
  from the URL only, so this tool takes none.
194
+
195
+ ## The client cut
196
+
197
+ The server instructions and every tool description are held to 2,048 characters
198
+ (`CLIENT_DESCRIPTION_CUT`, `src/requiredText.ts`), and the sentences a client must not lose sit in
199
+ the first 2,048 characters of each (`REQUIRED_IN_HEAD`). 2,048 is the shortest cut a client is
200
+ known to make:
201
+
202
+ - Claude Code from v2.1.280 sends the model at most `CLAUDE_CODE_MAX_MCP_DESCRIPTION_LENGTH`
203
+ characters of each description and of the instructions. Its documented default was 2,048 when
204
+ this budget was set (read 2026-10-05). On 2026-10-18 its documentation gives 4,096, and 16,384
205
+ for a tool it loads through tool search, and v2.1.302's debug log shows a 4,096 cut ("Tool
206
+ "exposure_radar" description truncated from 11807 to 4096 chars", from 2.6.8's 11,807-character
207
+ description). Builds between v2.1.280 and that change still cut at 2,048.
208
+ - Cursor cuts tool descriptions at 6,500 characters. VS Code and Claude Desktop document no cap.
209
+
210
+ Text that fits 2,048 therefore reaches each of them whole. The tests hold every served text to it
211
+ over stdio (`test/tools.test.mjs`) and over the hosted endpoint (`test/http.test.mjs`), and the
212
+ synthetic writes an `mcp text cut` line per transport and era that an alert reads.
213
+
214
+ ## How `check_sbom` and `scan_manifest` cut their rows
215
+
216
+ Past 30,000 characters of structured result, and without `full: true`, `check_sbom` and
217
+ `scan_manifest` cut `data` in this order (the note's `DATA CUT` sentences say where each answer
218
+ stopped):
219
+
220
+ 1. `scan_manifest`'s `not_checked`, then `not_sent_purls`, which is kept whole or not at all (the
221
+ note gives the positions of the purls not sent, so a second call can send them).
222
+ 2. Every row is thinned, every row kept: each keeps `index`, `purl`, `verdict`,
223
+ `not_assessed_reason`, `cve_ids`, `matched_package` and `matched_via` (and an upstream row's
224
+ `matched_version_from` and `purl_name`), and each match `cve_id`, `kev_listed`, `ransomware`,
225
+ `epss_score`, `effective_score`, `score_assessed`, `fixed_in` and `fixed_in_reason`: every field
226
+ a prompt reads from a match.
227
+ 3. Before any other row's matches are cut, the `not_affected` and `not_assessed` rows are cut to
228
+ `index`, `purl`, `verdict`, `not_assessed_reason` and `cve_ids`, and leave, the `not_affected`
229
+ ones first, as many as must for every other row to keep all its matches. `data.summary` counts
230
+ every verdict, and the note says how many left (#3269).
231
+ 4. Where the other rows' matches must be capped even with all of those rows gone, those rows stay
232
+ instead, cut so, while every other row fits at its floor: its matches used in ransomware or
233
+ KEV-listed, and 10 more (#3269's review). Each row's matches are capped at one number, the most
234
+ that fit, never under 10, and a row whose floor is above that number keeps its floor. A row
235
+ with more keeps the matches chosen used in ransomware first, then its other KEV-listed ones,
236
+ then those of highest `effective_score`, listed in the API's order, and carries
237
+ `matches_truncated`: its total, the number returned, and the KEV-listed and ransomware CVE IDs
238
+ and `fixed_in` versions of all its matches (#3270). Only where even the floor does not fit with
239
+ every one of those rows do they leave, as many as must for it to.
240
+ 5. Each match is cut to the fields of step 2 less `fixed_in_reason`, still capped as step 4 says:
241
+ every level that keeps matches keeps every field `workload_triage` ranks by. A null `fixed_in`
242
+ there without its `fixed_in_reason` is a reason cut for size, and `sbom_review` never calls it
243
+ no fix: it checks the purl again with `full: true`, or reads the fix from `cve_remediation` or
244
+ `get_cve`. Kept here, the reason cost the 201-purl probe a row (43 to 42).
245
+ 6. Where even that does not fit with all of those rows gone, no row's matches are cut below its
246
+ floor (#3269's second review): the other rows leave instead, whole, at step 5's level and
247
+ their floor. The rows kept are chosen those with a KEV-listed or ransomware match first, then
248
+ the others, each in input order, each one that fits beside those chosen before it, so a row
249
+ too large to fit beside them leaves and the rows after it can stay. The note names the
250
+ `affected` and `undetermined` rows that left by `index`, those with a KEV-listed or ransomware
251
+ match first and apart from the others (the first 100 indices or runs of them in all), and
252
+ `check_sbom`'s note says how to check them again (#3269's third review): "Re-check these K
253
+ purls in this order, in calls of 7, 5, 8, 8 and 16, then of at most 17: each such call keeps
254
+ every row in data." Each of the first five sizes is the most of the rows left, in that order,
255
+ that the cut's own measure counts as fitting at their floor with `FIRST_FIT_SLACK` (600
256
+ characters) held back, so a lower bound: a real call of them often keeps more. The last is the
257
+ most such that any that many of the rest in a row count so. Each is counted for those rows, as
258
+ the cut would cut such a call, with this call's note longer by what such a call's note names of
259
+ them (its affected components). A call of fewer of them in a row keeps every row too. A row whose floor alone is
260
+ past the budget goes in a call of its own. Where not even one row fits at its floor (one row
261
+ whose KEV-listed matches alone are past the budget), the row chosen first stays alone with the
262
+ most of its matches that fit, chosen as in step 4, and the note says so; where not one match
263
+ fits beside it, with none.
264
+ 7. Only where no row but the `not_affected` and `not_assessed` ones has a match is each row cut
265
+ further, to `index`, `purl`, `verdict`, `not_assessed_reason` and `cve_ids`, before rows leave.
266
+
267
+ Once the cut is settled, the rows kept take back detail in input order while room is left, and
268
+ then the rows steps 3 and 4 sent out come back, cut as step 3 says, as many as the room left
269
+ holds, where every other row stayed.
270
+
271
+ When rows leave `data`, the note names a page size N: "A call of the first N of these purls, or
272
+ of fewer, keeps every row in data; a call of other purls says in its own note what it leaves
273
+ out." (`scan_manifest`: a call whose files hold the first N of these purls.) N is counted for this
274
+ answer's own rows, as the cut above would cut a call of them, so it can be larger or smaller than
275
+ the number of rows this answer keeps; it is not a page size for the rest of the purls, whose rows
276
+ can be larger: round 2's "To read every row in data, check N or fewer purls per call" read as
277
+ one, and calls of the purls in chunks of N lost rows in 55% of such mixes (#3269's second review).
278
+ Nor is it a size for checking the rows left out again, which are the heavy ones: calls of N of
279
+ them dropped 504 of 2,540 rows on realistic Debian images (#3269's third review); the re-check
280
+ sentence of step 6 gives those sizes. `workload_triage` (its step 6) and `sbom_review` (its step
281
+ 1) check again the `affected` and `undetermined` rows the note names, those with a KEV-listed or
282
+ ransomware match first, in calls of the sizes that sentence gives, at most 5 calls, and report the
283
+ rest as unchecked; `workload_triage` takes its counts from `data.summary`, which counts every row
284
+ whatever the cut. `scan_manifest` names its rows left out the same way and gives no re-check sizes:
285
+ its rows are checked again through `check_sbom`, by purls its note does not give.
286
+
287
+ The first text block, past its own 30,000 characters, keeps the same priority: before any row is
288
+ cut to `index`, `purl`, `verdict`, `not_assessed_reason` and `cve_ids`, the `not_affected` and
289
+ `not_assessed` rows are cut to that and leave the text, as many as must, so an affected row keeps
290
+ its matches while room allows; only where that cannot fit is every row cut so, and then rows
291
+ leave, those two kinds first, and the rows with a KEV-listed or ransomware match last, as `data`
292
+ keeps them (#3284: through 3.1.0's review the text left the rest from the end in input order, so a
293
+ KEV-bearing row `data` kept could leave the text unnamed; on 36 realistic Debian 12 images a client
294
+ reading the text alone missed 95 of 413 such rows). The note names by `index` each of those that
295
+ still leaves ("Of the rows left out of it, 30 have a KEV-listed or ransomware match: at index 511
296
+ to 513, …"). The note says what those rows leave out, and that each string in
297
+ them keeps its first 100 characters where one is longer. The text names no page size (through
298
+ #3269's review it said "check N or fewer purls per call", N the rows it kept, and a call of the
299
+ first N purls kept fewer in 34 of 89 mixes); its note says how many rows it leaves out, of which
300
+ kinds, and that `structuredContent.data` holds them.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echelongraph-mcp",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "mcpName": "io.echelongraph/echelongraph-mcp",
5
5
  "description": "MCP server for CVE lookups: EchelonGraph scores, CISA KEV, EPSS, CWE, exploits, version and SBOM checks, vendor advisories, and per-CVE exposure derived from Shodan data. Shodan data is owned by Shodan, which holds its copyright (© Shodan). Keyless.",
6
6
  "type": "module",
package/server.json CHANGED
@@ -17,7 +17,7 @@
17
17
  ]
18
18
  }
19
19
  ],
20
- "version": "3.0.0",
20
+ "version": "3.1.0",
21
21
  "remotes": [
22
22
  {
23
23
  "type": "streamable-http",
@@ -28,7 +28,7 @@
28
28
  {
29
29
  "registryType": "npm",
30
30
  "identifier": "echelongraph-mcp",
31
- "version": "3.0.0",
31
+ "version": "3.1.0",
32
32
  "transport": {
33
33
  "type": "stdio"
34
34
  },