echelongraph-mcp 2.3.0 → 2.3.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/README.md CHANGED
@@ -17,15 +17,35 @@ API call a tool needs to answer.
17
17
 
18
18
  | Tool | What it does |
19
19
  |---|---|
20
- | `cve_summary` | Counts of active CVEs by severity, and when the feed was last updated. |
21
- | `search_cves` | Search/filter CVEs (severity, min CVSS, text, sort) with EchelonGraph scores. |
22
- | `get_cve` | Full record for one CVE: CVSS v3 and (when scored) v4, the EchelonGraph score and its confidence, EPSS, CISA-KEV status and known ransomware use, the GitHub GHSA id, references, and its published, modified and `updated_at` times. |
20
+ | `cve_summary` | Counts of active CVEs by severity band, the count with no severity band from any source (`summary.none`, sent again as `summary.unscored`: CVEs not yet scored, not a rating of None), and when the feed was last updated. |
21
+ | `search_cves` | Search/filter CVEs (severity, min CVSS, text, sort) with EchelonGraph scores and `score_assessed`; the note names each row not yet scored. |
22
+ | `get_cve` | Full record for one CVE: CVSS v3 and (when scored) v4, the EchelonGraph score and its confidence, whether EchelonGraph has scored it (`score_assessed`), EPSS, CISA-KEV status and known ransomware use, the GitHub GHSA id, references, and its published, modified and `updated_at` times. |
23
23
  | `cve_exposure` | Internet-exposure footprint for a CVE: exposed service count (distinct ip:port, the `exposed_hosts` field) + country/product breakdown, from the KEV-exposure radar. |
24
24
  | `exposure_radar` | Aggregate totals across the exposure radars: services running CISA-KEV CVEs; unauthenticated data stores and observability UIs, found through Shodan (LeakIX when Shodan query credits run low) and then confirmed by EchelonGraph's own identified check, which is not a pure read (on Redis it names its client; on ClickHouse its query lands in the server's query log); leaked credentials; shadow AI; and MCP servers found in EchelonGraph's own Certificate Transparency feed, by RFC 9728 verdict, protocol era and transport. Every number is labelled by what it counts, and a field the tool cannot label is left out and named. |
25
25
 
26
26
  Every figure is as fresh as the schedule that refreshes it. The CVE feed is polled from its
27
27
  sources on a schedule; each radar refreshes on its own.
28
28
 
29
+ ### How `get_cve` and `search_cves` read the EchelonGraph score
30
+
31
+ `echelongraph_score` (0 to 10), its band `echelongraph_severity` and the risk priority
32
+ `echelongraph_risk` (0 to 100) are EchelonGraph's score only when the record's
33
+ `score_assessed` is `true`. A CVE EchelonGraph has not scored carries `score_assessed: false`,
34
+ and it is **not yet scored**, not scored 0. The API leaves those three fields out on it, or (an
35
+ API before that change) sends `0`, `NONE` and `0` as placeholders, which are not a rating and do
36
+ not mean the CVE is harmless. Its `score_confidence` is `NONE`, and `score_unassessed_reason`
37
+ says why: no source has yet published severity data EchelonGraph can score, or the record was
38
+ rejected (withdrawn) by its numbering authority, and a rejected record is never scored.
39
+
40
+ Both tools relay the API's JSON as it was sent, and the note says what the score is, for
41
+ `get_cve` per CVE and for `search_cves` per row, naming each CVE it is about:
42
+
43
+ | API answer | What the note says |
44
+ |---|---|
45
+ | `score_assessed: true` | Nothing more: the score is a score. |
46
+ | `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. |
47
+ | 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. |
48
+
29
49
  ### How `cve_exposure` counts
30
50
 
31
51
  Every 12 hours, when Shodan query credits allow, the KEV-exposure radar runs one Shodan query
package/dist/index.js CHANGED
@@ -332,6 +332,80 @@ const realInstant = (s) => s !== undefined && Date.parse(s) > 0;
332
332
  // Stats, in memory), and is relayed in data but not presented as the feed's freshness.
333
333
  const CVE_FEED_METHOD = "EchelonGraph's CVE Pulse feed: CVE records compiled from NVD, MITRE-CNA pre-NVD records, CISA-KEV, EPSS and GitHub GHSA, each polled from its source on a schedule.";
334
334
  const NO_FEED_FRESHNESS = "freshness is null: the CVE feed's answer carries no time at which its pollers last completed a poll for the feed as a whole.";
335
+ function scoreStateOf(rec) {
336
+ const v = boolAt(rec, "score_assessed");
337
+ return v === true ? "assessed" : v === false ? "not_assessed" : "unstated";
338
+ }
339
+ const scoreRejected = (rec) => strAt(rec, "score_unassessed_reason") === "rejected";
340
+ // The fields that hold a placeholder, not a score, on a CVE that is not scored.
341
+ const PLACEHOLDER_FIELDS = ["echelongraph_score", "echelongraph_severity", "echelongraph_risk"];
342
+ // No sentence below writes a digit: a note that says "0" beside "score" is the defect itself.
343
+ const UNSTATED_SCORE = "echelongraph_score is EchelonGraph's score only when score_assessed is true, so a zero echelongraph_score there is not a rating: it can be the placeholder a CVE not yet scored carries.";
344
+ // get_cve's sentences about the one CVE's score, each starting with a space; none when the
345
+ // answer says the CVE is scored.
346
+ function cveScoreNote(cve, rec) {
347
+ const state = scoreStateOf(rec);
348
+ if (state === "assessed")
349
+ return "";
350
+ if (state === "unstated") {
351
+ return ` The answer does not say whether EchelonGraph has scored ${cve}: it carries no score_assessed (an API older than that field sends none). ${UNSTATED_SCORE}`;
352
+ }
353
+ const rejected = scoreRejected(rec);
354
+ const reportAs = rejected ? "not scored" : "not yet scored";
355
+ const head = rejected
356
+ ? ` (score_assessed: false) NOT SCORED: the record of ${cve} was rejected (withdrawn) by its numbering authority (score_unassessed_reason rejected), and EchelonGraph does not score a withdrawn record, so the record holds no EchelonGraph score for it.`
357
+ : ` (score_assessed: false) NOT YET SCORED: EchelonGraph has not yet scored ${cve}, so the record holds no EchelonGraph score for it, and the absence of one does not mean ${cve} is harmless.`;
358
+ const why = strAt(rec, "score_unassessed_reason") === "no_signal" ? " score_unassessed_reason is no_signal: no source has yet published severity data EchelonGraph can score." : "";
359
+ const carried = PLACEHOLDER_FIELDS.filter((k) => field(rec, k) !== undefined && field(rec, k) !== null);
360
+ const placeholders = carried.length
361
+ ? ` ${listed(carried)} ${carried.length === 1 ? "is a placeholder" : "are placeholders"}, not a score: report ${cve} as ${reportAs}, and do not repeat ${carried.length === 1 ? "that value" : "those values"} as its score.`
362
+ : ` Report ${cve} as ${reportAs}, not as a zero or low score.`;
363
+ return `${head}${why}${placeholders}`;
364
+ }
365
+ // search_cves' sentences about its rows' scores, naming each row a sentence is about, each
366
+ // starting with a space; none when every row says it is scored.
367
+ function rowsScoreNote(d) {
368
+ const rows = field(d, "cves");
369
+ if (!Array.isArray(rows) || rows.length === 0)
370
+ return "";
371
+ const n = rows.length;
372
+ const ids = (pred) => rows.flatMap((r, i) => (pred(r) ? [strAt(r, "cve_id") ?? `cves[${i}]`] : []));
373
+ const notYet = ids((r) => scoreStateOf(r) === "not_assessed" && !scoreRejected(r));
374
+ const rejected = ids((r) => scoreStateOf(r) === "not_assessed" && scoreRejected(r));
375
+ const unstated = ids((r) => scoreStateOf(r) === "unstated");
376
+ const of = (k) => (k < n ? `${k} of the ${n} CVEs in this page` : n === 1 ? "the 1 CVE in this page" : `all ${n} CVEs in this page`);
377
+ const out = [];
378
+ if (notYet.length) {
379
+ out.push(`(score_assessed: false) NOT YET SCORED: ${of(notYet.length)} ${be(notYet.length)} not yet scored by EchelonGraph: ${listed(notYet)}. For each, echelongraph_score, echelongraph_severity and echelongraph_risk, where its row carries them, are placeholders, not a score, and the absence of a score does not mean the CVE is harmless: report each as not yet scored, not as a zero or low score.`);
380
+ }
381
+ if (rejected.length) {
382
+ out.push(`(score_assessed: false) NOT SCORED: ${of(rejected.length)} ${rejected.length === 1 ? "has a record" : "have records"} rejected (withdrawn) by the numbering authority (score_unassessed_reason rejected), which EchelonGraph does not score: ${listed(rejected)}. For each, echelongraph_score, echelongraph_severity and echelongraph_risk, where its row carries them, are placeholders, not a score: report each as not scored.`);
383
+ }
384
+ if (unstated.length) {
385
+ out.push(`The answer does not say whether EchelonGraph has scored ${of(unstated.length)}: ${listed(unstated)} (no score_assessed in ${unstated.length === 1 ? "its row" : "their rows"}; an API older than that field sends none). ${UNSTATED_SCORE}`);
386
+ }
387
+ return out.map((s) => ` ${s}`).join("");
388
+ }
389
+ // ── summary.none, the not-yet-scored bucket (#2610) ──
390
+ //
391
+ // core-backend cve/store.go Summary buckets the active CVEs on effectiveSeverityExpr: the
392
+ // EchelonGraph band, else NVD's, else the CVSS v2 band, else NONE, where a band of NONE or UNKNOWN
393
+ // counts as none. So summary.none counts the CVEs with no severity band from any source, and the
394
+ // store sends the same number again as summary.unscored (CVESummary: "the residual band-less
395
+ // bucket … numerically identical to None"; Critical+High+Medium+Low+Unscored == Total). It is not
396
+ // a count of CVEs rated severity None, but a model handed "counts by severity … none" reports
397
+ // "3,381 CVEs rated None" (production on 2026-09-30: none 3,381, unscored 3,381). The JSON is
398
+ // relayed as sent; the note says what the count is whenever it is above zero, and names
399
+ // summary.unscored as the same count when the answer carries it equal. A zero is not labelled:
400
+ // there is no CVE in it to misreport.
401
+ function summaryNoneNote(d) {
402
+ const none = numAt(d, "summary", "none");
403
+ if (none === undefined || none <= 0)
404
+ return "";
405
+ const unscored = numAt(d, "summary", "unscored");
406
+ const same = unscored === none ? ` summary.unscored (${unscored}) is the same count under its own name.` : "";
407
+ return ` summary.none (${none}) is not a severity rating of None: it counts the active CVEs with no severity band from any source, that is, CVEs not yet scored.${same} Report them as not yet scored, not as CVEs rated None.`;
408
+ }
335
409
  async function cveSummary() {
336
410
  const tool = "cve_summary";
337
411
  try {
@@ -354,14 +428,15 @@ async function cveSummary() {
354
428
  ],
355
429
  };
356
430
  const total = numAt(r.data, "summary", "total");
431
+ const none = summaryNoneNote(r.data);
357
432
  if (total === undefined)
358
- return succeeded(r.data, head, env);
433
+ return succeeded(r.data, `${head}${none}`, env);
359
434
  if (total === 0) {
360
- return succeeded(r.data, `${head} The feed reports 0 active CVEs — a measured empty result (we looked and found nothing), not a lookup failure.`, env);
435
+ return succeeded(r.data, `${head} The feed reports 0 active CVEs — a measured empty result (we looked and found nothing), not a lookup failure.${none}`, env);
361
436
  }
362
437
  // A stamp that is not a real instant stays in the JSON and is not repeated as prose.
363
438
  const stamp = realInstant(updated) ? ` (last updated ${updated})` : "";
364
- return succeeded(r.data, `${head} The feed holds ${total} active CVEs${stamp}.`, env);
439
+ return succeeded(r.data, `${head} The feed holds ${total} active CVEs${stamp}.${none}`, env);
365
440
  }
366
441
  catch (e) {
367
442
  return crashed(tool, e);
@@ -411,21 +486,24 @@ async function searchCVEs(a) {
411
486
  const relaxedNote = relaxed === true
412
487
  ? " The API relaxed the search phrase to all of its words (search_relaxed is true), so these rows are a superset of the rows that match the phrase itself."
413
488
  : "";
489
+ // Then what each row's score is (#2535): empty when every row says it is scored, or there
490
+ // are no rows.
491
+ const tail = `${relaxedNote}${rowsScoreNote(r.data)}`;
414
492
  // A total the API did not count is not a count: the page's own rows are all that is known.
415
493
  if (counted === false) {
416
494
  const empty = shown === 0 && (offset ?? 0) === 0;
417
495
  return succeeded(r.data, empty
418
- ? `${head} The query returned 0 CVEs — a measured empty result: EchelonGraph was queried successfully and nothing matched these filters (we looked and found nothing). This is not a lookup failure.${relaxedNote}`
419
- : `${head} The API did not count the matches (total_counted is false), so its total is not a count${shown === undefined ? "" : `; ${shown} returned in this page`}.${relaxedNote}`, env);
496
+ ? `${head} The query returned 0 CVEs — a measured empty result: EchelonGraph was queried successfully and nothing matched these filters (we looked and found nothing). This is not a lookup failure.${tail}`
497
+ : `${head} The API did not count the matches (total_counted is false), so its total is not a count${shown === undefined ? "" : `; ${shown} returned in this page`}.${tail}`, env);
420
498
  }
421
499
  const total = numAt(r.data, "total") ?? shown;
422
500
  if (total === undefined)
423
- return succeeded(r.data, `${head}${relaxedNote}`, env);
501
+ return succeeded(r.data, `${head}${tail}`, env);
424
502
  if (total === 0 && floor !== true) {
425
- return succeeded(r.data, `${head} The query matched 0 CVEs — a measured empty result: EchelonGraph was queried successfully and nothing matched these filters (we looked and found nothing). This is not a lookup failure.${relaxedNote}`, env);
503
+ return succeeded(r.data, `${head} The query matched 0 CVEs — a measured empty result: EchelonGraph was queried successfully and nothing matched these filters (we looked and found nothing). This is not a lookup failure.${tail}`, env);
426
504
  }
427
505
  const matched = floor === true ? `at least ${total} CVEs (total_is_lower_bound is true: the count stopped at that floor)` : `${total} CVEs`;
428
- return succeeded(r.data, `${head} The query matched ${matched}${shown === undefined ? "" : `; ${shown} returned in this page`}.${relaxedNote}`, env);
506
+ return succeeded(r.data, `${head} The query matched ${matched}${shown === undefined ? "" : `; ${shown} returned in this page`}.${tail}`, env);
429
507
  }
430
508
  catch (e) {
431
509
  return crashed(tool, e);
@@ -441,7 +519,8 @@ async function getCVE(cve_id) {
441
519
  if (!r.ok)
442
520
  return failed(tool, r);
443
521
  const written = strAt(r.data, "updated_at");
444
- return succeeded(r.data, `${okHead(tool, r.status)} Returned the record for ${strAt(r.data, "cve_id") ?? id}.`, {
522
+ const cve = strAt(r.data, "cve_id") ?? id;
523
+ return succeeded(r.data, `${okHead(tool, r.status)} Returned the record for ${cve}.${cveScoreNote(cve, r.data)}`, {
445
524
  state: "measured",
446
525
  measured_at: instantOrNull(written),
447
526
  method: CVE_FEED_METHOD,
@@ -1567,9 +1646,13 @@ const CVERecord = z.looseObject({
1567
1646
  cvss_v3_score: opt(z.number()),
1568
1647
  cvss_v4_score: opt(z.number()),
1569
1648
  cvss_v4_severity: opt(z.string()),
1570
- echelongraph_score: opt(z.number()),
1571
- echelongraph_severity: opt(z.string()),
1649
+ // #2535: the score is a score only beside score_assessed true (see cveScoreNote).
1650
+ echelongraph_score: opt(z.number()).describe("EchelonGraph's 0-10 score for the CVE: a score only when score_assessed is true. With score_assessed false it is a placeholder, not a rating."),
1651
+ echelongraph_severity: opt(z.string()).describe("The severity band of echelongraph_score: a rating only when score_assessed is true. With score_assessed false it is a placeholder (NONE)."),
1652
+ echelongraph_risk: opt(z.number()).describe("EchelonGraph's 0-100 risk priority, fused from the score, exploitation and automatability: a rating only when score_assessed is true. With score_assessed false it is a placeholder."),
1572
1653
  score_confidence: opt(z.string()),
1654
+ score_assessed: opt(z.boolean()).describe("Whether EchelonGraph has scored the CVE. true: echelongraph_score, echelongraph_severity and echelongraph_risk are its score. false: the CVE is NOT YET SCORED, not scored 0, and any of those three it carries is a placeholder, not a rating; the API may leave them out. Absent (an API older than the field): the answer does not say whether the CVE was scored."),
1655
+ score_unassessed_reason: opt(z.string()).describe("Why score_assessed is false: no_signal, no source has yet published severity data EchelonGraph can score; or rejected, the record was withdrawn by its numbering authority and is never scored. Absent when the CVE is scored."),
1573
1656
  epss_score: opt(z.number()),
1574
1657
  epss_percentile: opt(z.number()),
1575
1658
  kev_listed: opt(z.boolean()),
@@ -1590,7 +1673,9 @@ const CVE_SUMMARY_OUTPUT = envelopeSchema({
1590
1673
  high: opt(z.number()),
1591
1674
  medium: opt(z.number()),
1592
1675
  low: opt(z.number()),
1593
- none: opt(z.number()),
1676
+ // #2610: the not-yet-scored bucket, not a rating (see summaryNoneNote).
1677
+ none: opt(z.number()).describe("The active CVEs with no severity band from any source: CVEs not yet scored, not a severity rating of None."),
1678
+ unscored: opt(z.number()).describe("The same count as none, under its own name."),
1594
1679
  last_updated: opt(z.string()),
1595
1680
  })
1596
1681
  .optional(),
@@ -1756,19 +1841,25 @@ const ANNOTATIONS = { readOnlyHint: true, destructiveHint: false, idempotentHint
1756
1841
  const CVE_ID_ARG = z.string().describe("a CVE ID, e.g. CVE-2023-44487");
1757
1842
  // What each description says about its structured result, naming only fields its schema holds.
1758
1843
  const FEED_ENVELOPE = "Its structured result carries state (measured), measured_at, method, coverage, freshness (null: the feed serves no poll-completion time) and notes, with data equal to the API's JSON; the result's last text block repeats it without data (the first text block) and without the note's sentences (the text block before it), with which notes ends.";
1844
+ // #2610: what cve_summary says summary.none is (summaryNoneNote), one constant string for the same
1845
+ // reason as SCORE_ASSESSED_DESCRIPTION below.
1846
+ const SUMMARY_NONE_DESCRIPTION = "summary.none is not a severity rating of None: it counts the active CVEs with no severity band from any source, that is, CVEs not yet scored, and the answer may carry the same count again as summary.unscored. Whenever summary.none is above zero the note says so: report those CVEs as not yet scored, not as CVEs rated None.";
1847
+ // #2535: what search_cves and get_cve say about score_assessed, one constant string, since the
1848
+ // site's tool-claims check (marketing-site lib/mcpToolClaims.test.ts) folds only constant strings.
1849
+ const SCORE_ASSESSED_DESCRIPTION = "echelongraph_score, echelongraph_severity and echelongraph_risk are EchelonGraph's score only when score_assessed is true. With score_assessed false the CVE is NOT YET SCORED, not scored 0: any of those three it carries (0, NONE, 0) is a placeholder, not a rating, and does not mean the CVE is harmless; the API may leave them out instead, score_confidence is NONE, and score_unassessed_reason says why (a rejected record, withdrawn by its numbering authority, is never scored, and the note says NOT SCORED). The note labels each such CVE NOT YET SCORED: report it that way, never as a score of 0. An answer with no score_assessed (an API older than that field) does not say whether the CVE was scored, the note says so, and a 0 there is not a rating either.";
1759
1850
  // One server, built per connection by serveStdio for whichever era the client opens with.
1760
1851
  // tools/list answers in registration order, which is the order below.
1761
1852
  function createServer() {
1762
1853
  const server = new McpServer({ name: NAME, version: VERSION }, { instructions: INSTRUCTIONS, capabilities: { tools: { listChanged: false } } });
1763
1854
  server.registerTool("cve_summary", {
1764
1855
  title: "CVE feed summary",
1765
- description: `Summary of EchelonGraph's CVE Pulse feed: summary.total active CVEs and their counts by severity (summary.critical, summary.high, summary.medium, summary.low, summary.none), plus summary.last_updated, the newest modification time among those records. The feed is polled from its sources on a schedule, so this is the state as of that update. ${FEED_ENVELOPE}`,
1856
+ description: `Summary of EchelonGraph's CVE Pulse feed: summary.total active CVEs, their counts by severity band (summary.critical, summary.high, summary.medium, summary.low), the count with no band (summary.none), and summary.last_updated, the newest modification time among those records. ${SUMMARY_NONE_DESCRIPTION} The feed is polled from its sources on a schedule, so this is the state as of that update. ${FEED_ENVELOPE}`,
1766
1857
  outputSchema: CVE_SUMMARY_OUTPUT,
1767
1858
  annotations: ANNOTATIONS,
1768
1859
  }, async () => checked("cve_summary", CVE_SUMMARY_OUTPUT, await cveSummary()));
1769
1860
  server.registerTool("search_cves", {
1770
1861
  title: "Search CVEs",
1771
- 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. Returns cves, each with cve_id, severity, cvss_v3_score, echelongraph_score, 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. ${FEED_ENVELOPE} coverage repeats total, total_counted, total_is_lower_bound, search_relaxed, limit and offset, and gives returned, the rows in this page.`,
1862
+ 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. 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.`,
1772
1863
  inputSchema: z.object({
1773
1864
  search: z.string().optional().describe("free-text search (product, vendor, or keyword, e.g. 'tomcat')"),
1774
1865
  severity: z.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW"]).optional().describe("filter to one severity"),
@@ -1781,7 +1872,7 @@ function createServer() {
1781
1872
  }, async (a) => checked("search_cves", SEARCH_CVES_OUTPUT, await searchCVEs(a)));
1782
1873
  server.registerTool("get_cve", {
1783
1874
  title: "CVE detail",
1784
- description: `Full record for one CVE: 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), 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, published, modified and updated_at; each field only where the record has it. Pass a CVE ID like CVE-2023-44487. ${FEED_ENVELOPE}`,
1875
+ description: `Full record for one CVE: 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, 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}`,
1785
1876
  inputSchema: z.object({ cve_id: CVE_ID_ARG }),
1786
1877
  outputSchema: GET_CVE_OUTPUT,
1787
1878
  annotations: ANNOTATIONS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echelongraph-mcp",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "mcpName": "io.echelongraph/echelongraph-mcp",
5
5
  "description": "EchelonGraph CVE and internet-exposure data as an MCP server: CVEs with EchelonGraph multi-source scores, CISA-KEV and EPSS, plus a per-CVE internet-exposure footprint derived from Shodan data. Shodan data is owned by Shodan, which holds its copyright (© Shodan). Free and keyless, for Claude, Cursor and any MCP client.",
6
6
  "type": "module",
package/server.json CHANGED
@@ -8,12 +8,12 @@
8
8
  "url": "https://github.com/echelongraph/echelongraph-mcp",
9
9
  "source": "github"
10
10
  },
11
- "version": "2.3.0",
11
+ "version": "2.3.2",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "echelongraph-mcp",
16
- "version": "2.3.0",
16
+ "version": "2.3.2",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },