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.
@@ -36,11 +36,21 @@
36
36
  // CVE-2024-21412, which both vendors did publish advisories for. core-backend now reads those
37
37
  // histories back (vendoradv backfill.go) and serves each vendor's window at
38
38
  // /api/v1/public/vendor-advisories/coverage (coverage.go): advisories held, the earliest and latest
39
- // vendor_published_at among them, and history_backfill. All three tools read it beside their own
40
- // request and relay it in coverage, and the note says when a CVE's ID year begins before a
41
- // vendor's earliest held advisory, or a vendor's history is still being read, so an empty answer
42
- // is never read as "the vendor published none". The window is context, not the answer: when it
43
- // cannot be read (an API older than the route, a failure), the answer is relayed as before, with
39
+ // vendor_published_at among them, held_since and history_backfill. All three tools read it beside
40
+ // their own request and relay it in coverage, and the note says when a CVE's ID year begins before
41
+ // the date a vendor is held from, or a vendor's history is still being read, so an empty answer
42
+ // is never read as "the vendor published none".
43
+ //
44
+ // held_since, not the earliest held advisory, is where a vendor's window begins (#2803). A vendor
45
+ // with no history read (history_backfill not_supported) is held from its poller's first poll on,
46
+ // and Cisco's and Red Hat's first polls read only what the vendor had CHANGED lately: an old
47
+ // advisory the vendor revised since is held, so the earliest held advisory reached back to 2024
48
+ // for Cisco and 2009 for Red Hat while the advisories published beside it are not held, and
49
+ // CVE-2025-20188 read Cisco as fully held for 2025. coverage.go serves held_since as the start of
50
+ // the window EchelonGraph has read the vendor's advisories from; an API older than that field gives none, and a vendor is
51
+ // then taken as held from its earliest advisory only when its history read is complete.
52
+ //
53
+ // The window is context, not the answer: when it cannot be read (an API older than the route, a failure), the answer is relayed as before, with
44
54
  // the windows null and a note saying they are unknown, never failed. The coverage request carries
45
55
  // nothing typed: a fixed path, no query, no search header.
46
56
  //
@@ -113,6 +123,7 @@ function windowsOf(d) {
113
123
  advisories: num(r, "advisories") ?? null,
114
124
  earliest_vendor_published_at: instantOrNull(r, "earliest_vendor_published_at"),
115
125
  latest_vendor_published_at: instantOrNull(r, "latest_vendor_published_at"),
126
+ held_since: instantOrNull(r, "held_since"),
116
127
  history_backfill: str(r, "history_backfill") ?? null,
117
128
  },
118
129
  ];
@@ -120,30 +131,48 @@ function windowsOf(d) {
120
131
  }
121
132
  const day = (instant) => instant.slice(0, 10);
122
133
  const backfilling = (w) => w.history_backfill !== null && BACKFILLING.has(w.history_backfill);
134
+ // #2803: the start of the window a vendor's advisories have been read from: held_since as the API
135
+ // gives it; from an API without it, the earliest held advisory when the vendor's history read is
136
+ // complete (that is the archive's start), else unknown (null).
137
+ const heldFrom = (w) => w.held_since ?? (w.history_backfill === "complete" ? w.earliest_vendor_published_at : null);
138
+ // Whether a vendor's window starts on or before 1 January of a CVE ID's year.
139
+ const heldFromYear = (w, yearStart) => {
140
+ const from = heldFrom(w);
141
+ return from !== null && Date.parse(from) <= yearStart;
142
+ };
123
143
  // What a note says of one window: what is held, and the history read when it is not done.
124
144
  const windowItem = (w, heldOnly) => {
145
+ const from = heldFrom(w);
146
+ const to = w.latest_vendor_published_at ? ` to ${day(w.latest_vendor_published_at)}` : "";
125
147
  const held = w.earliest_vendor_published_at === null
126
148
  ? "none held"
127
- : `held ${heldOnly ? "only " : ""}from ${day(w.earliest_vendor_published_at)}${w.latest_vendor_published_at ? ` to ${day(w.latest_vendor_published_at)}` : ""}`;
149
+ : from === null
150
+ ? `held from ${day(w.earliest_vendor_published_at)}${to}, not known to be without a gap`
151
+ : `held ${heldOnly ? "only " : ""}from ${day(from)}${to}${day(w.earliest_vendor_published_at) < day(from) ? `, and before that only in part, the earliest held dated ${day(w.earliest_vendor_published_at)}` : ""}`;
128
152
  return `${w.vendor} (${held}${backfilling(w) ? `; history_backfill ${w.history_backfill}` : ""})`;
129
153
  };
130
154
  // The sentence for windows that could not be read: no "none here" is a finding about a vendor.
131
155
  const windowsUnknown = (why) => ` The vendors' coverage windows could not be read (${why}), so this answer cannot say from what date each vendor's advisories are held: no advisory here from a vendor is not a finding that it published none.`;
132
156
  // vendor_advisories_for_cve: the vendors with no advisory in the answer that may have published
133
- // one EchelonGraph does not hold: none held at all, the earliest held dated after 1 January of the
134
- // year in the CVE ID, or a history still being read.
157
+ // one EchelonGraph does not hold: none held at all, held (held_since) only from after 1 January of
158
+ // the year in the CVE ID or from a date not known, or a history still being read. Never the
159
+ // earliest held advisory: for a vendor with no history read it can be years before held_since
160
+ // (#2803).
135
161
  function notFullyHeld(windows, rows, year) {
136
162
  const answered = new Set(rows.map((r) => str(r, "vendor")).filter((v) => v !== undefined));
137
163
  const yearStart = Date.UTC(year, 0, 1);
138
- return windows.filter((w) => !answered.has(w.vendor) && (w.earliest_vendor_published_at === null || Date.parse(w.earliest_vendor_published_at) > yearStart || backfilling(w)));
164
+ return windows.filter((w) => !answered.has(w.vendor) && (w.earliest_vendor_published_at === null || !heldFromYear(w, yearStart) || backfilling(w)));
139
165
  }
140
166
  function notFullyHeldNote(cve, year, missing) {
141
167
  if (!missing.length)
142
168
  return "";
143
169
  const k = missing.length;
144
170
  const yearStart = Date.UTC(year, 0, 1);
145
- const items = missing.map((w) => windowItem(w, w.earliest_vendor_published_at !== null && Date.parse(w.earliest_vendor_published_at) > yearStart));
146
- return ` ${cve} is a ${year} CVE ID, and EchelonGraph may not hold every advisory ${plural(k, "1 vendor", `${k} vendors`)} published for it (vendors_not_fully_held): ${items.join("; ")}. An advisory a vendor published before the earliest one EchelonGraph holds is not held, and while history_backfill is in_progress or not_started the vendor's older advisories are still being read, so no advisory here from ${plural(k, "that vendor", "those vendors")} is not a finding that ${plural(k, "it", "they")} published none.`;
171
+ const items = missing.map((w) => {
172
+ const from = heldFrom(w);
173
+ return windowItem(w, from !== null && Date.parse(from) > yearStart);
174
+ });
175
+ return ` ${cve} is a ${year} CVE ID, and EchelonGraph may not hold every advisory ${plural(k, "1 vendor", `${k} vendors`)} published for it (vendors_not_fully_held): ${items.join("; ")}. An advisory a vendor published before the date EchelonGraph holds that vendor from (held_since) may not be held, even when an older one is, and while history_backfill is in_progress or not_started the vendor's older advisories are still being read, so no advisory here from ${plural(k, "that vendor", "those vendors")} is not a finding that ${plural(k, "it", "they")} published none.`;
147
176
  }
148
177
  // search_vendor_advisories and get_vendor_advisory: the vendors whose history is still being read.
149
178
  function backfillingNote(windows) {
@@ -179,8 +208,10 @@ const DATES_DESCRIPTION = "Each advisory carries vendor_published_at (the vendor
179
208
  const ENVELOPE_DESCRIPTION = "Its structured result carries state (measured), measured_at, method, coverage, freshness (null: the answer carries 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.";
180
209
  // #2729: what a vendor's window is.
181
210
  const HISTORY_BACKFILL_DESCRIPTION = "history_backfill: complete (the vendor's published history has been read back as far as its source goes), in_progress or not_started (it is still being read, so the vendor's older advisories are not all held yet), or not_supported (EchelonGraph has no history read for that vendor, so what it holds is what the vendor's feed has carried)";
182
- const WINDOW_DESCRIPTION = `vendor, advisories (how many EchelonGraph holds from that vendor), earliest_vendor_published_at and latest_vendor_published_at (the earliest and latest vendor_published_at among them, null when none is held) and ${HISTORY_BACKFILL_DESCRIPTION}`;
183
- const NOT_HELD_DESCRIPTION = "An advisory a vendor published before its earliest_vendor_published_at is not held, so no advisory from a vendor is not a finding that it published none.";
211
+ // #2803: where a vendor's window begins.
212
+ const HELD_SINCE_DESCRIPTION = "held_since (the start of the window EchelonGraph has read that vendor's advisories from, before which an advisory may not be held; null when none is held or the API does not say; for a vendor with no history read it is when its feed was first read from, and can be years after earliest_vendor_published_at, since an older advisory is held when the vendor revised it later, not the ones published beside it)";
213
+ const WINDOW_DESCRIPTION = `vendor, advisories (how many EchelonGraph holds from that vendor), earliest_vendor_published_at and latest_vendor_published_at (the earliest and latest vendor_published_at among them, null when none is held), ${HELD_SINCE_DESCRIPTION} and ${HISTORY_BACKFILL_DESCRIPTION}`;
214
+ const NOT_HELD_DESCRIPTION = "An advisory a vendor published before its held_since may not be held, even when its earliest_vendor_published_at is older, so no advisory from a vendor is not a finding that it published none.";
184
215
  // #2728: how a search matches, and its capped total.
185
216
  const SEARCH_MATCH_DESCRIPTION = "A query of 3 or more characters is matched case-insensitively as a substring of each advisory's title, description, vendor name, vendor_advisory_id, affected_products and cve_ids (search_match substring). A query of 1 or 2 characters matches whole words only (search_match word): it must equal, ignoring case, a whole word (a run of letters and digits) of one of those, so xz finds xz-utils but not xzibit, and a 1- or 2-character query with any other character, such as c#, matches nothing.";
186
217
  const TOTAL_CAPPED_DESCRIPTION = "A search counts at most 1,000 matches: past that, total is 1000 and total_capped is true, which means 1,000 or more (the note writes 1,000+), never exactly 1,000; paging past it still works. Without a query, total is the exact count and total_capped is false.";
@@ -191,8 +222,12 @@ const VendorWindowSchema = z.strictObject({
191
222
  earliest_vendor_published_at: z
192
223
  .string()
193
224
  .nullable()
194
- .describe("The earliest vendor_published_at among them; null when none is held. An advisory this vendor published before it is not held."),
225
+ .describe("The earliest vendor_published_at among them; null when none is held. Not where the window begins: see held_since."),
195
226
  latest_vendor_published_at: z.string().nullable().describe("The latest vendor_published_at among them; null when none is held."),
227
+ held_since: z
228
+ .string()
229
+ .nullable()
230
+ .describe("The start of the window EchelonGraph has read this vendor's advisories from; null when none is held or the API does not say. An advisory this vendor published before it may not be held, even when earliest_vendor_published_at is older."),
196
231
  // The four values the API sends today, and any later one as a string: a new state must not turn
197
232
  // the answer into a failure.
198
233
  history_backfill: z
@@ -210,7 +245,9 @@ const ROW_CUT_LEVELS = [
210
245
  { keep: ["vendor", "vendor_display_name", "vendor_advisory_id", "title", "severity", "cvss_v3_score", "cve_ids", "summary", "affected_products", "vendor_published_at", "withdrawn"], clip: 200, cap: 20 },
211
246
  { keep: ["vendor", "vendor_advisory_id", "title", "severity", "cve_ids", "vendor_published_at", "withdrawn"], clip: 100, cap: 10 },
212
247
  ];
213
- const WHOLE = "get_vendor_advisory returns any one of these advisories whole, by its vendor and vendor_advisory_id.";
248
+ // get_vendor_advisory's own first text block is cut too past the budget (#2802): only its
249
+ // structuredContent.data is one advisory whole.
250
+ const WHOLE = "get_vendor_advisory returns any one of these advisories by its vendor and vendor_advisory_id, whole in its structuredContent.data, and in its first text block with every field (past 30,000 characters, each list cut to its first entries).";
214
251
  const FOR_CVE_TEXT = { rows: "advisories", levels: ROW_CUT_LEVELS, whole: WHOLE };
215
252
  const searchText = (a) => ({
216
253
  rows: "advisories",
@@ -231,7 +268,7 @@ export function registerVendorAdvisoryTools(server, kit) {
231
268
  vendors_not_fully_held: z
232
269
  .array(z.string())
233
270
  .nullable()
234
- .describe("The vendors with no advisory in this answer that may have published one EchelonGraph does not hold: none held, the earliest held dated after 1 January of cve_year, or history_backfill in_progress or not_started. No advisory from them is not a finding that they published none. null when the windows could not be read."),
271
+ .describe("The vendors with no advisory in this answer that may have published one EchelonGraph does not hold: none held, held_since after 1 January of cve_year or not known, or history_backfill in_progress or not_started. No advisory from them is not a finding that they published none. null when the windows could not be read."),
235
272
  }),
236
273
  freshness: null,
237
274
  });
@@ -471,7 +508,7 @@ export function registerVendorAdvisoryTools(server, kit) {
471
508
  }
472
509
  server.registerTool("vendor_advisories_for_cve", {
473
510
  title: "Vendor advisories for one CVE",
474
- description: `The vendor-published advisories that name one CVE, newest first, at most 20: for each, vendor, vendor_display_name, vendor_advisory_id, title, severity and cvss_v3_score where the vendor gives them. Covers the vendor feeds EchelonGraph polls, for example Microsoft MSRC, Red Hat, Cisco, Palo Alto Networks and GitHub GHSA; an empty answer means that none of the advisories EchelonGraph holds from those feeds names the CVE, not that no vendor published one (see vendor_windows and vendors_not_fully_held). ${DATES_DESCRIPTION} Pass a CVE ID like CVE-2024-21412. coverage gives returned, cap and at_cap (true: the answer is full, so there may be more); cve_year, the year in the CVE ID; vendor_windows, each vendor's window in what EchelonGraph holds: ${WINDOW_DESCRIPTION}; and vendors_not_fully_held, the vendors with no advisory in the answer of which EchelonGraph holds none, whose earliest held advisory is dated after 1 January of cve_year, or whose history is still being read, which the note names. ${NOT_HELD_DESCRIPTION} vendor_windows and vendors_not_fully_held are null when the windows could not be read, and the note says so. measured_at is null. ${ENVELOPE_DESCRIPTION} ${TEXT_BUDGET_DESCRIPTION}${ROW_CUT_DESCRIPTION}.`,
511
+ description: `The vendor-published advisories that name one CVE, newest first, at most 20: for each, vendor, vendor_display_name, vendor_advisory_id, title, severity and cvss_v3_score where the vendor gives them. Covers the vendor feeds EchelonGraph polls, for example Microsoft MSRC, Red Hat, Cisco, Palo Alto Networks and GitHub GHSA; an empty answer means that none of the advisories EchelonGraph holds from those feeds names the CVE, not that no vendor published one (see vendor_windows and vendors_not_fully_held). ${DATES_DESCRIPTION} Pass a CVE ID like CVE-2024-21412. coverage gives returned, cap and at_cap (true: the answer is full, so there may be more); cve_year, the year in the CVE ID; vendor_windows, each vendor's window in what EchelonGraph holds: ${WINDOW_DESCRIPTION}; and vendors_not_fully_held, the vendors with no advisory in the answer of which EchelonGraph holds none, whose held_since is after 1 January of cve_year or not known, or whose history is still being read, which the note names. ${NOT_HELD_DESCRIPTION} vendor_windows and vendors_not_fully_held are null when the windows could not be read, and the note says so. measured_at is null. ${ENVELOPE_DESCRIPTION} ${TEXT_BUDGET_DESCRIPTION}${ROW_CUT_DESCRIPTION}.`,
475
512
  inputSchema: z.object({ cve_id: z.string().describe("a CVE ID, e.g. CVE-2024-21412") }),
476
513
  outputSchema: FOR_CVE_OUTPUT,
477
514
  annotations: kit.annotations,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "echelongraph-mcp",
3
- "version": "2.6.3",
3
+ "version": "2.6.6",
4
4
  "mcpName": "io.echelongraph/echelongraph-mcp",
5
- "description": "MCP server for CVE and vulnerability lookups: CVE records with EchelonGraph multi-source scores, CISA KEV, EPSS history, CWE weaknesses, public exploits, version and SBOM checks against the vulnerability advisory corpus, vendor security advisories, and 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.",
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",
7
7
  "bin": {
8
8
  "echelongraph-mcp": "dist/index.js"
package/server.json CHANGED
@@ -17,7 +17,7 @@
17
17
  ]
18
18
  }
19
19
  ],
20
- "version": "2.6.3",
20
+ "version": "2.6.6",
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.3",
31
+ "version": "2.6.6",
32
32
  "transport": {
33
33
  "type": "stdio"
34
34
  },