echelongraph-mcp 2.3.1 → 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,7 +17,7 @@ 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. |
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
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
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. |
package/dist/index.js CHANGED
@@ -386,6 +386,26 @@ function rowsScoreNote(d) {
386
386
  }
387
387
  return out.map((s) => ` ${s}`).join("");
388
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
+ }
389
409
  async function cveSummary() {
390
410
  const tool = "cve_summary";
391
411
  try {
@@ -408,14 +428,15 @@ async function cveSummary() {
408
428
  ],
409
429
  };
410
430
  const total = numAt(r.data, "summary", "total");
431
+ const none = summaryNoneNote(r.data);
411
432
  if (total === undefined)
412
- return succeeded(r.data, head, env);
433
+ return succeeded(r.data, `${head}${none}`, env);
413
434
  if (total === 0) {
414
- 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);
415
436
  }
416
437
  // A stamp that is not a real instant stays in the JSON and is not repeated as prose.
417
438
  const stamp = realInstant(updated) ? ` (last updated ${updated})` : "";
418
- 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);
419
440
  }
420
441
  catch (e) {
421
442
  return crashed(tool, e);
@@ -1652,7 +1673,9 @@ const CVE_SUMMARY_OUTPUT = envelopeSchema({
1652
1673
  high: opt(z.number()),
1653
1674
  medium: opt(z.number()),
1654
1675
  low: opt(z.number()),
1655
- 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."),
1656
1679
  last_updated: opt(z.string()),
1657
1680
  })
1658
1681
  .optional(),
@@ -1818,6 +1841,9 @@ const ANNOTATIONS = { readOnlyHint: true, destructiveHint: false, idempotentHint
1818
1841
  const CVE_ID_ARG = z.string().describe("a CVE ID, e.g. CVE-2023-44487");
1819
1842
  // What each description says about its structured result, naming only fields its schema holds.
1820
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.";
1821
1847
  // #2535: what search_cves and get_cve say about score_assessed, one constant string, since the
1822
1848
  // site's tool-claims check (marketing-site lib/mcpToolClaims.test.ts) folds only constant strings.
1823
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.";
@@ -1827,7 +1853,7 @@ function createServer() {
1827
1853
  const server = new McpServer({ name: NAME, version: VERSION }, { instructions: INSTRUCTIONS, capabilities: { tools: { listChanged: false } } });
1828
1854
  server.registerTool("cve_summary", {
1829
1855
  title: "CVE feed summary",
1830
- 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}`,
1831
1857
  outputSchema: CVE_SUMMARY_OUTPUT,
1832
1858
  annotations: ANNOTATIONS,
1833
1859
  }, async () => checked("cve_summary", CVE_SUMMARY_OUTPUT, await cveSummary()));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echelongraph-mcp",
3
- "version": "2.3.1",
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.1",
11
+ "version": "2.3.2",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "echelongraph-mcp",
16
- "version": "2.3.1",
16
+ "version": "2.3.2",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },