echelongraph-mcp 2.6.7 → 2.6.8

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,27 @@ 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.8 — 2026-10-05
10
+
11
+ - get_cve and search_cves now say what `patch_available` means, and describe `patch_evidence`
12
+ (#2861). Through 2.6.7 neither tool described `patch_available`, and core-backend set it only
13
+ from Ubuntu and Red Hat patch records, so a `false` read as "no patch": measured in production on
14
+ 2026-10-05, 101,725 CVEs read `false` while EchelonGraph held evidence of a fix for them, 48,145
15
+ with a package `fixed_version` and 71,705 with an NVD reference tagged "Patch" (CVE-2017-5651,
16
+ Apache Tomcat, among them, with two package rows carrying a `fixed_version`). core-backend now
17
+ sets `patch_available` true whenever it holds evidence of a fix, and serves `patch_evidence`, the
18
+ list of its sources, strongest first: `vendor_patch` (a vendor patch on record, from Ubuntu or
19
+ Red Hat), `fixed_version` (a package fix version, or the fixed bound of an affected version
20
+ range) and `nvd_patch_reference` (an NVD reference tagged Patch). Both descriptions, and the
21
+ outputSchema descriptions of both fields, now state: `patch_available` is true when EchelonGraph
22
+ holds evidence of a fix; `false` means no fix evidence is on record, which is not a finding that
23
+ no fix exists; once true it is never set back to false, so it can be true beside an empty
24
+ `patch_evidence`; and an answer with no `patch_evidence` (an API older than that field) does not
25
+ name the evidence. Both fields are relayed as the API sends them; a `patch_evidence` label the API
26
+ adds later is relayed, not a failed call.
27
+ - search_cves: a page cut to fit the text keeps `patch_evidence` beside `patch_available` in each
28
+ row at the first cut level.
29
+
9
30
  ## 2.6.7 — 2026-10-05
10
31
 
11
32
  - Tool descriptions now state facts only: no reporting instructions to the model and no references
package/README.md CHANGED
@@ -317,6 +317,18 @@ Both tools relay the API's JSON as it was sent, and the note says what the score
317
317
  | `score_assessed: false` | **NOT YET SCORED**, tagged "(score_assessed: false)": report the CVE as not yet scored, never as a zero or low score. Each of `echelongraph_score`, `echelongraph_severity` and `echelongraph_risk` it carries is named as a placeholder. A rejected record is **NOT SCORED** instead. |
318
318
  | no `score_assessed` field | The answer does not say whether the CVE was scored (an API older than the field), and a zero `echelongraph_score` in it is not a rating. |
319
319
 
320
+ ### What `patch_available` says
321
+
322
+ `patch_available` is `true` when EchelonGraph holds evidence of a fix for the CVE, and
323
+ `patch_evidence` lists the sources of that evidence, strongest first: `vendor_patch` (a vendor
324
+ patch on record, from Ubuntu or Red Hat), `fixed_version` (a fixed version recorded for an
325
+ affected package, or the fixed bound of an affected version range) and `nvd_patch_reference` (an
326
+ NVD reference tagged Patch). `false` means no fix evidence is on record, which is **not** a
327
+ finding that no fix exists. Once `true`, `patch_available` is not set back to `false`, so it can be
328
+ `true` beside an empty `patch_evidence`. An answer with no `patch_evidence` (an API older than that
329
+ field) does not name the evidence, and a `false` there is not a finding that no fix exists either.
330
+ Both tools relay the two fields as the API sends them.
331
+
320
332
  ### How `cve_exposure` counts
321
333
 
322
334
  Every 12 hours, when Shodan query credits allow, the KEV-exposure radar runs one Shodan query
package/dist/index.js CHANGED
@@ -652,7 +652,7 @@ const SEARCH_CVES_TEXT = {
652
652
  rows: "cves",
653
653
  levels: [
654
654
  {
655
- keep: ["cve_id", "description", "severity", "cvss_v3_score", "echelongraph_score", "echelongraph_severity", "echelongraph_risk", "score_assessed", "score_unassessed_reason", "epss_score", "kev_listed", "kev_ransomware", "published", "vuln_status", "patch_available", "exploit_poc_available"],
655
+ keep: ["cve_id", "description", "severity", "cvss_v3_score", "echelongraph_score", "echelongraph_severity", "echelongraph_risk", "score_assessed", "score_unassessed_reason", "epss_score", "kev_listed", "kev_ransomware", "published", "vuln_status", "patch_available", "patch_evidence", "exploit_poc_available"],
656
656
  clip: 200,
657
657
  },
658
658
  { keep: ["cve_id", "description", "severity", "cvss_v3_score", "echelongraph_score", "score_assessed", "score_unassessed_reason", "epss_score", "kev_listed", "published"], clip: 100 },
@@ -1946,6 +1946,23 @@ export const opt = (t) => t.nullable().optional();
1946
1946
  // tool-claims check folds only constant strings).
1947
1947
  const CPE_MATCH_DESCRIPTION = "The record's CPE match criteria as one flat list, every configuration's cpeMatch entries together: the AND/OR operator and negate of each configuration and node, versionStartExcluding and matchCriteriaId are not in it, so a CPE that is only a platform a product runs on reads as one more entry.";
1948
1948
  const CPE_CONFIGURATIONS_DESCRIPTION = "NVD's configurations array as NVD sent it: each configuration and node with its operator (AND, OR) and negate, and each cpeMatch with criteria, vulnerable, versionStartIncluding, versionStartExcluding, versionEndIncluding, versionEndExcluding and matchCriteriaId. A cpeMatch with vulnerable false inside an AND configuration is the platform the vulnerable product runs on, not an affected product. Absent: EchelonGraph holds no stored NVD configuration for the CVE, which is not a finding that no product is affected.";
1949
+ // #2861: what patch_available and patch_evidence are, as core-backend serves them since 2d8bb9ce
1950
+ // (core-backend/internal/cve/patch_evidence.go states the rule). Through that commit
1951
+ // patch_available was EXISTS(a cve_patches row), fed only by Ubuntu and Red Hat, so 101,725 CVEs
1952
+ // read false while EchelonGraph held a package fixed_version or an NVD Patch-tagged reference for
1953
+ // them (CVE-2017-5651 among them). patch_available is now true exactly when the evidence set is
1954
+ // non-empty, and only ever set true. So false is "no fix evidence on record", never "no fix
1955
+ // exists", and an answer without patch_evidence (an API older than it) says nothing about why.
1956
+ // One constant string each (the site's tool-claims check folds only constant strings).
1957
+ const PATCH_EVIDENCE_LABELS = ["vendor_patch", "fixed_version", "nvd_patch_reference"];
1958
+ const PATCH_EVIDENCE_SOURCES = "vendor_patch (a vendor patch on record, from Ubuntu or Red Hat), fixed_version (a fixed version recorded for an affected package, or the fixed bound of an affected version range) and nvd_patch_reference (an NVD reference tagged Patch)";
1959
+ const PATCH_AVAILABLE_DESCRIPTION = "patch_available is true when EchelonGraph holds evidence of a fix for the CVE, and patch_evidence names the sources of that evidence, strongest first: " +
1960
+ PATCH_EVIDENCE_SOURCES +
1961
+ ". patch_available false means no fix evidence is on record, which is not a finding that no fix exists. patch_evidence lists the evidence found when EchelonGraph last computed it, and is empty where none was found; patch_available, once true, is never set back to false, so it can be true beside an empty patch_evidence. An answer with no patch_evidence (an API older than that field) does not name the evidence, and a false there is not a finding that no fix exists either.";
1962
+ const PATCH_AVAILABLE_FIELD = "true: EchelonGraph holds evidence of a fix for the CVE, its sources named in patch_evidence. false: no fix evidence is on record, which is not a finding that no fix exists. Once true it is never set back to false.";
1963
+ const PATCH_EVIDENCE_FIELD = "The sources of the fix evidence EchelonGraph holds for the CVE, strongest first: " +
1964
+ PATCH_EVIDENCE_SOURCES +
1965
+ ". Empty where none was found when EchelonGraph last computed it. Absent (an API older than the field): the answer does not name the evidence.";
1949
1966
  const CVERecord = z.looseObject({
1950
1967
  cve_id: opt(z.string()),
1951
1968
  description: opt(z.string()),
@@ -1966,6 +1983,9 @@ const CVERecord = z.looseObject({
1966
1983
  kev_ransomware: opt(z.boolean()),
1967
1984
  kev_added_date: opt(z.string()),
1968
1985
  ghsa_id: opt(z.string()),
1986
+ // #2861: fix evidence. A label the API adds later is admitted, never a failed call.
1987
+ patch_available: opt(z.boolean()).describe(PATCH_AVAILABLE_FIELD),
1988
+ patch_evidence: opt(z.array(z.union([z.enum(PATCH_EVIDENCE_LABELS), z.string()]))).describe(PATCH_EVIDENCE_FIELD),
1969
1989
  references: z.unknown().optional(),
1970
1990
  // #2720: NVD's configurations, losslessly, beside the flat list (see cpeConfigurationsNote).
1971
1991
  cpe_match: z.unknown().optional().describe(CPE_MATCH_DESCRIPTION),
@@ -2222,7 +2242,7 @@ export function createServer() {
2222
2242
  }, async () => checked("cve_summary", CVE_SUMMARY_OUTPUT, await cveSummary()));
2223
2243
  server.registerTool("search_cves", {
2224
2244
  title: "Search CVEs",
2225
- description: `Search/list CVEs from EchelonGraph's CVE feed (NVD + MITRE-CNA pre-NVD + CISA-KEV + EPSS + GitHub GHSA, each polled on a schedule). Filter by severity, minimum CVSS, free text, and sort; page with limit and offset. Returns cves, each with cve_id, severity, cvss_v3_score, echelongraph_score and score_assessed (whether EchelonGraph has scored it), epss_score and kev_listed where the record has them, and the list's total, total_counted (false: the matches were not counted, so total is not a count), total_is_lower_bound (true: at least total), search_relaxed (true: a phrase was relaxed to all of its words), limit and offset. ${SCORE_ASSESSED_DESCRIPTION} ${FEED_ENVELOPE} coverage repeats total, total_counted, total_is_lower_bound, search_relaxed, limit and offset, and gives returned, the rows in this page. ${TEXT_BUDGET_DESCRIPTION} Cut, each row keeps at least the fields named above and the first 200 characters of its description (100 on a page too long for that), or rows are left out and the note gives the offset to call next.`,
2245
+ description: `Search/list CVEs from EchelonGraph's CVE feed (NVD + MITRE-CNA pre-NVD + CISA-KEV + EPSS + GitHub GHSA, each polled on a schedule). Filter by severity, minimum CVSS, free text, and sort; page with limit and offset. Returns cves, each with cve_id, severity, cvss_v3_score, echelongraph_score and score_assessed (whether EchelonGraph has scored it), epss_score and kev_listed where the record has them, and the list's total, total_counted (false: the matches were not counted, so total is not a count), total_is_lower_bound (true: at least total), search_relaxed (true: a phrase was relaxed to all of its words), limit and offset. ${SCORE_ASSESSED_DESCRIPTION} ${FEED_ENVELOPE} coverage repeats total, total_counted, total_is_lower_bound, search_relaxed, limit and offset, and gives returned, the rows in this page. ${TEXT_BUDGET_DESCRIPTION} Cut, each row keeps at least the fields named above and the first 200 characters of its description (100 on a page too long for that), or rows are left out and the note gives the offset to call next. ${PATCH_AVAILABLE_DESCRIPTION}`,
2226
2246
  inputSchema: z.object({
2227
2247
  search: z.string().optional().describe("free-text search (product, vendor, or keyword, e.g. 'tomcat')"),
2228
2248
  severity: z.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW"]).optional().describe("filter to one severity"),
@@ -2237,7 +2257,7 @@ export function createServer() {
2237
2257
  }, async (a) => checked("search_cves", SEARCH_CVES_OUTPUT, await searchCVEs(a)));
2238
2258
  server.registerTool("get_cve", {
2239
2259
  title: "CVE detail",
2240
- description: `One CVE's record: description, severity, cvss_v3_score, cvss_v4_score and cvss_v4_severity (when the record has a CVSS v4 score), echelongraph_score and echelongraph_severity with score_confidence (EchelonGraph's multi-source score) and score_assessed (whether EchelonGraph has scored it), epss_score and epss_percentile, CISA-KEV status (kev_listed, kev_added_date, and kev_ransomware for known ransomware-campaign use), ghsa_id (GitHub GHSA), references, cpe_match (the CPE criteria as one flat list) and cpe_configurations (NVD's configurations as NVD sent them, with each AND/OR operator, negate, versionStartExcluding and matchCriteriaId; absent where EchelonGraph has stored none, which is not a finding that no product is affected), published, modified and updated_at; each field only where the record has it. Pass a CVE ID like CVE-2023-44487. ${SCORE_ASSESSED_DESCRIPTION} ${FEED_ENVELOPE} ${TEXT_BUDGET_DESCRIPTION} Cut, each list in the record keeps its first entries, and the note names each list cut with its full length.`,
2260
+ description: `One CVE's record: description, severity, cvss_v3_score, cvss_v4_score and cvss_v4_severity (when the record has a CVSS v4 score), echelongraph_score and echelongraph_severity with score_confidence (EchelonGraph's multi-source score) and score_assessed (whether EchelonGraph has scored it), epss_score and epss_percentile, CISA-KEV status (kev_listed, kev_added_date, and kev_ransomware for known ransomware-campaign use), ghsa_id (GitHub GHSA), patch_available and patch_evidence (whether EchelonGraph holds evidence of a fix, and its sources), references, cpe_match (the CPE criteria as one flat list) and cpe_configurations (NVD's configurations as NVD sent them, with each AND/OR operator, negate, versionStartExcluding and matchCriteriaId; absent where EchelonGraph has stored none, which is not a finding that no product is affected), published, modified and updated_at; each field only where the record has it. Pass a CVE ID like CVE-2023-44487. ${SCORE_ASSESSED_DESCRIPTION} ${PATCH_AVAILABLE_DESCRIPTION} ${FEED_ENVELOPE} ${TEXT_BUDGET_DESCRIPTION} Cut, each list in the record keeps its first entries, and the note names each list cut with its full length.`,
2241
2261
  inputSchema: z.object({ cve_id: CVE_ID_ARG }),
2242
2262
  outputSchema: GET_CVE_OUTPUT,
2243
2263
  annotations: ANNOTATIONS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echelongraph-mcp",
3
- "version": "2.6.7",
3
+ "version": "2.6.8",
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": "2.6.7",
20
+ "version": "2.6.8",
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": "2.6.7",
31
+ "version": "2.6.8",
32
32
  "transport": {
33
33
  "type": "stdio"
34
34
  },