echelongraph-mcp 2.1.0 → 2.2.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/README.md +18 -11
- package/dist/index.js +64 -29
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -171,7 +171,7 @@ outage for an all-clear:
|
|
|
171
171
|
|
|
172
172
|
- **Success** — the first text block is the API's JSON verbatim; the second is a one-line
|
|
173
173
|
note saying the call succeeded, which base URL answered, and what it found; the third is
|
|
174
|
-
the structured result
|
|
174
|
+
the structured result as JSON, less what the first two already say (below). When the
|
|
175
175
|
feed genuinely holds nothing for the query the note says so in words ("we looked and
|
|
176
176
|
found nothing … not a lookup failure"), because a measured zero is a measurement. The
|
|
177
177
|
exception to verbatim is `exposure_radar`: each radar is cut to the fields listed above, and
|
|
@@ -191,12 +191,13 @@ outage for an all-clear:
|
|
|
191
191
|
the host could not be reached, it answered non-2xx, it took longer than the timeout, or
|
|
192
192
|
it answered 2xx with a body that is not a JSON object. The text names the tool, the
|
|
193
193
|
cause (status code or error kind), the path, and the base URL, and says it is not a
|
|
194
|
-
finding; the second text block is the structured result
|
|
195
|
-
rendered as a success with null fields.
|
|
194
|
+
finding; the second text block is the structured result as JSON, less the message's
|
|
195
|
+
sentences (below). A failure is never rendered as a success with null fields.
|
|
196
196
|
|
|
197
|
-
Both shapes also carry a structured result, below, and repeat it in their last text block,
|
|
198
|
-
client that passes only `content` to the model still
|
|
199
|
-
first text block (the API's JSON, or the failure) and the
|
|
197
|
+
Both shapes also carry a structured result, below, and repeat it in their last text block, less
|
|
198
|
+
what an earlier block already says, so a client that passes only `content` to the model still
|
|
199
|
+
sees how the answer was measured. The first text block (the API's JSON, or the failure) and the
|
|
200
|
+
note after it are where 1.x put them.
|
|
200
201
|
|
|
201
202
|
## Structured results
|
|
202
203
|
|
|
@@ -216,15 +217,21 @@ declares its shape as an `outputSchema` in `tools/list`, with a title and the an
|
|
|
216
217
|
| `data` | On a success only: the same JSON as the first text block. |
|
|
217
218
|
| `error` | On a failure only: `kind`, `path`, `status` and `message`. |
|
|
218
219
|
|
|
219
|
-
The last text block of every result is this structured result serialized as JSON,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
The last text block of every result is this structured result serialized as JSON, less what
|
|
221
|
+
an earlier text block already says verbatim, since a client may pass only `content` to the
|
|
222
|
+
model. It carries the same `state`, `measured_at`, `coverage` and `freshness` (and on a failure
|
|
223
|
+
`error`), key for key. It leaves out `data`, which is a success's first text block, and the
|
|
224
|
+
note's sentences, which are the text block just before it (on a failure, the message) and
|
|
225
|
+
with which `notes` ends: its `notes` holds only the sentences the envelope adds about itself,
|
|
226
|
+
and is left out when there are none. It leaves out `method` too where that note quotes it verbatim, as
|
|
227
|
+
`cve_exposure`'s does ("Method: …"). So the text blocks together carry the whole structured
|
|
228
|
+
result, and cannot disagree with it. Before 2.2.0 this block repeated the whole note, so every
|
|
229
|
+
note was sent twice.
|
|
223
230
|
|
|
224
231
|
| `state` | What it means |
|
|
225
232
|
|---|---|
|
|
226
233
|
| `measured` | A measurement of what was asked. An exposure count is `measured` only when the answer says when what it counts was observed (`measured_at`) and how it was produced (`method`). No exposure answer says when today, so neither exposure tool answers `measured`. |
|
|
227
|
-
| `not_assessed` | The answer holds no dated measurement of what was asked, so no
|
|
234
|
+
| `not_assessed` | The answer holds no dated measurement of what was asked, so no count in it is presented as one: the radar does not look for this CVE, the answer does not say whether it does, or the answer does not say when what it counts was observed. It can still relay a count, as what the source holds on record, undated: `cve_exposure`'s `exposed_hosts` when its `exposure_state` is `exposed`, and `exposure_radar`'s labelled totals. The notes, and `exposure_state` where the result carries it, say what each count is. |
|
|
228
235
|
| `failed` | The lookup did not complete. Not a finding. |
|
|
229
236
|
| `invalid_input` | The input was refused, by this server or by the API, so nothing was looked up. Not a finding. |
|
|
230
237
|
|
package/dist/index.js
CHANGED
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
// nothing ("we looked and found nothing" is a measurement). One exception: exposure_radar
|
|
14
14
|
// relays each radar's answer cut to the fields it can label by what they count, and names
|
|
15
15
|
// what it left out (readKEVExposure, readExposedDatabases, readLeakedCredentials and
|
|
16
|
-
// readShadowAI; #2307, #2313). content[2] is the envelope below, as JSON
|
|
16
|
+
// readShadowAI; #2307, #2313). content[2] is the envelope below, as JSON, less what
|
|
17
|
+
// content[0] and the note already say.
|
|
17
18
|
// - failure (isError: true): the lookup did not complete — unreachable host, non-2xx,
|
|
18
19
|
// timeout, or a 2xx whose body is not a JSON object — named by tool, cause and base URL,
|
|
19
|
-
// in content[0]; content[1] is the envelope below, as JSON.
|
|
20
|
+
// in content[0]; content[1] is the envelope below, as JSON, less the message's sentences.
|
|
20
21
|
// Never a success with null fields: a model handed one of those tells its user "no
|
|
21
22
|
// exposure found", and an outage becomes an all-clear.
|
|
22
23
|
//
|
|
@@ -26,13 +27,16 @@
|
|
|
26
27
|
// with, on a success, `data` equal to what content[0] carries, and on a failure an `error`.
|
|
27
28
|
// Each tool advertises the envelope as its outputSchema.
|
|
28
29
|
//
|
|
29
|
-
// The envelope in the text (#2440). Many clients pass only `content` to the model, so the
|
|
30
|
-
// text block of every result is structuredContent serialized as JSON,
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
30
|
+
// The envelope in the text (#2440, #2467). Many clients pass only `content` to the model, so the
|
|
31
|
+
// last text block of every result is structuredContent serialized as JSON, less what an earlier
|
|
32
|
+
// text block already carries verbatim: `data` (content[0] on a success), the note's sentences
|
|
33
|
+
// (the block just before, which structuredContent's notes end with), and method where that note
|
|
34
|
+
// quotes it. The rest — state, measured_at, coverage, freshness, the envelope's own notes and,
|
|
35
|
+
// on a failure, error — is copied key for key, so the text blocks together are the whole of
|
|
36
|
+
// structuredContent and cannot disagree with it. The MCP spec (2025-06-18, Tools, Structured
|
|
37
|
+
// Content) asks a tool that returns structured content to return it serialized in a text block
|
|
38
|
+
// too; the JSON block stays first and the note second, as in 1.x, so a client that parses
|
|
39
|
+
// content[0] is unaffected.
|
|
36
40
|
//
|
|
37
41
|
// Protocol eras (#2311). serveStdio answers both: a 2026-07-28 client's server/discover and
|
|
38
42
|
// per-request `_meta` envelope, and a 2025-era client's `initialize` handshake. The first
|
|
@@ -206,20 +210,39 @@ const NO_RECORD = "The API returned no record for this request; the quoted messa
|
|
|
206
210
|
// It is tagged invalid_input, the same state a request this server refuses locally carries.
|
|
207
211
|
const INVALID_INPUT = "(state: invalid_input)";
|
|
208
212
|
const REJECTED_INPUT = "The API rejected the request as invalid input, so nothing was looked up: this is not a finding. Do not report it as zero, none found, or unexposed; correct the input and retry.";
|
|
209
|
-
// The envelope as a text block (#2440)
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
// so
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
// The envelope as a text block (#2440), so a client that passes only `content` to the model
|
|
214
|
+
// still sees how the answer was measured: structuredContent serialized as JSON, less what an
|
|
215
|
+
// earlier text block already carries verbatim (#2467). 2.1.0 sent the whole envelope without
|
|
216
|
+
// `data`, so every note went out twice, once as prose and once as the notes array, and
|
|
217
|
+
// cve_exposure's method a third time.
|
|
218
|
+
// data left out: content[0] carries it verbatim.
|
|
219
|
+
// notes only the envelope's own sentences (`own`), which no text block says; the rest of
|
|
220
|
+
// structuredContent's notes are the sentences of `said`, the text block just before
|
|
221
|
+
// this one (the note, or a failure's message). Left out when there are none.
|
|
222
|
+
// method left out when `said` quotes it verbatim (cve_exposure's note, "Method: …").
|
|
223
|
+
// Every other key is copied as is, in structuredContent's order, so the text blocks together are
|
|
224
|
+
// the whole of structuredContent and cannot disagree with it.
|
|
225
|
+
const envelopeText = (structured, said, own) => {
|
|
226
|
+
const envelope = {};
|
|
227
|
+
for (const [k, v] of Object.entries(structured)) {
|
|
228
|
+
if (k === "data")
|
|
229
|
+
continue;
|
|
230
|
+
if (k === "notes") {
|
|
231
|
+
if (own.length)
|
|
232
|
+
envelope.notes = own;
|
|
233
|
+
}
|
|
234
|
+
else if (!(k === "method" && typeof v === "string" && said.includes(v)))
|
|
235
|
+
envelope[k] = v;
|
|
236
|
+
}
|
|
215
237
|
return text(JSON.stringify(envelope, null, 2));
|
|
216
238
|
};
|
|
217
239
|
// A failure result: the #1874 text, and an envelope saying nothing was measured, in
|
|
218
240
|
// structuredContent and again as the last text block. The error quotes only what
|
|
219
|
-
// describeFailure already quotes (a redacted cause, the API's own message).
|
|
241
|
+
// describeFailure already quotes (a redacted cause, the API's own message). Every note is a
|
|
242
|
+
// sentence of the message, so the text envelope carries none.
|
|
220
243
|
const failure = (state, message, error, coverage = null) => {
|
|
221
244
|
const structuredContent = { state, measured_at: null, method: null, coverage, freshness: null, notes: sentences(message), error };
|
|
222
|
-
return { content: [text(message), envelopeText(structuredContent)], structuredContent, isError: true };
|
|
245
|
+
return { content: [text(message), envelopeText(structuredContent, message, [])], structuredContent, isError: true };
|
|
223
246
|
};
|
|
224
247
|
const errorOf = (f) => ({ kind: f.kind, path: f.path, status: f.status ?? null, message: f.detail });
|
|
225
248
|
const failed = (tool, f) => {
|
|
@@ -264,11 +287,12 @@ const checked = (tool, schema, r) => {
|
|
|
264
287
|
const p = schema.safeParse(r.structuredContent);
|
|
265
288
|
return p.success ? r : unexpectedShape(tool, p.error);
|
|
266
289
|
};
|
|
267
|
-
// A success: content[0] is the data, content[1] the note, content[2] the envelope
|
|
290
|
+
// A success: content[0] is the data, content[1] the note, content[2] the envelope less both;
|
|
268
291
|
// the envelope carries both, the note as sentences after any the envelope adds about itself.
|
|
269
292
|
const succeeded = (data, note, env) => {
|
|
270
|
-
const
|
|
271
|
-
|
|
293
|
+
const own = env.notes ?? [];
|
|
294
|
+
const structuredContent = { ...env, notes: [...own, ...sentences(note)], data };
|
|
295
|
+
return { content: [text(JSON.stringify(data, null, 2)), text(note), envelopeText(structuredContent, note, own)], structuredContent };
|
|
272
296
|
};
|
|
273
297
|
const okHead = (tool, status) => `${tool} OK: EchelonGraph answered${status === undefined ? "" : ` HTTP ${status}`} from ${SHOWN_BASE}.`;
|
|
274
298
|
// Tolerant readers for the note: a missing or renamed field degrades the sentence, never
|
|
@@ -464,7 +488,7 @@ const SHODAN_ATTRIBUTION = "Exposure counts are derived from Shodan data.";
|
|
|
464
488
|
// and copyright in the applicable Shodan materials" (#2306). The attribution above does the
|
|
465
489
|
// first; this sentence, placed beside it wherever Shodan data is named, does the second.
|
|
466
490
|
const SHODAN_OWNERSHIP = "Shodan data is owned by Shodan, which holds its copyright (© Shodan).";
|
|
467
|
-
const CVE_EXPOSURE_DESCRIPTION = `Internet-exposure footprint for one CVE from EchelonGraph's KEV-exposure radar: how many internet-facing services (distinct ip:port, returned as exposed_hosts; a machine answering on two ports counts twice) the radar has on record running a version its CVE matcher maps to this CVE, with a country/product breakdown and a ransomware flag. Aggregate and host-redacted; free and keyless. Method: exposure counts are derived from Shodan data. ${SHODAN_OWNERSHIP} Every 12 h, when Shodan query credits allow, the radar runs one Shodan query per tracked product, reads up to 100 ip:port services per query, and keeps a service when its banner version matches a CISA-KEV or high-EPSS CVE; a service whose row has not been written or refreshed for 21 days is dropped. last_seen is when EchelonGraph last wrote or refreshed a service's row, not when the service was observed and not when its vulnerable version was last confirmed: it is set to the time of the write when a search matches the banner, and again when a re-check finds the port still listed by Shodan InternetDB, without re-reading the banner, so a patched service can stay counted while its port stays open. A count is therefore a banner-version inference over a sample, not an exploit test and not an internet-wide census. The radar only looks for its tracked set of CVEs: for a CVE outside that set the
|
|
491
|
+
const CVE_EXPOSURE_DESCRIPTION = `Internet-exposure footprint for one CVE from EchelonGraph's KEV-exposure radar: how many internet-facing services (distinct ip:port, returned as exposed_hosts; a machine answering on two ports counts twice) the radar has on record running a version its CVE matcher maps to this CVE, with a country/product breakdown and a ransomware flag. Aggregate and host-redacted; free and keyless. Method: exposure counts are derived from Shodan data. ${SHODAN_OWNERSHIP} Every 12 h, when Shodan query credits allow, the radar runs one Shodan query per tracked product, reads up to 100 ip:port services per query, and keeps a service when its banner version matches a CISA-KEV or high-EPSS CVE; a service whose row has not been written or refreshed for 21 days is dropped. last_seen is when EchelonGraph last wrote or refreshed a service's row, not when the service was observed and not when its vulnerable version was last confirmed: it is set to the time of the write when a search matches the banner, and again when a re-check finds the port still listed by Shodan InternetDB, without re-reading the banner, so a patched service can stay counted while its port stays open. A count is therefore a banner-version inference over a sample, not an exploit test and not an internet-wide census. The radar only looks for its tracked set of CVEs: for a CVE outside that set the note says NOT ASSESSED (exposure_state not_assessed), and its 0 is not a measurement.`;
|
|
468
492
|
function exposureNote(head, id, d) {
|
|
469
493
|
const cve = strAt(d, "cve_id") ?? id;
|
|
470
494
|
const hosts = numAt(d, "exposed_hosts");
|
|
@@ -1184,7 +1208,7 @@ const EXPOSURE_RADAR_DESCRIPTION = `Aggregate totals from EchelonGraph's interne
|
|
|
1184
1208
|
// and freshness carries each radar's last_run_at where the API serves one.
|
|
1185
1209
|
const RADAR_METHOD = "Aggregate counts over what each radar holds on record: kev_exposure, Shodan banners whose version maps to a CISA-KEV-listed CVE; exposed_databases, services found through Shodan (LeakIX when Shodan query credits run low) and confirmed by EchelonGraph's own identified check; leaked_credentials, credential-shaped strings in public GitHub push events; shadow_ai, services found through Certificate Transparency logs and Shodan and checked by EchelonGraph's identified probes.";
|
|
1186
1210
|
const RADAR_NAMES = RADARS.map(([name]) => name);
|
|
1187
|
-
const RADAR_STATE_NOTE = "state is not_assessed: every radar answered, but no stats answer says when the services or records it counts were observed, so no count here is presented as a dated measurement and measured_at is null. Each count is what that radar holds on record,
|
|
1211
|
+
const RADAR_STATE_NOTE = "state is not_assessed: every radar answered, but no stats answer says when the services or records it counts were observed, so no count here is presented as a dated measurement and measured_at is null. Each count is what that radar holds on record, and the note labels each one by what it counts.";
|
|
1188
1212
|
const RADAR_FRESHNESS_NOTE = "freshness gives each radar's last completed check where the API serves one (freshness.kev_exposure.last_run_at, freshness.exposed_databases.last_run_at, freshness.leaked_credentials.last_run_at, and freshness.shadow_ai.last_run_at with freshness.shadow_ai.running), and null where it does not.";
|
|
1189
1213
|
async function exposureRadar() {
|
|
1190
1214
|
const tool = "exposure_radar";
|
|
@@ -1262,11 +1286,19 @@ const ErrorSchema = z.strictObject({
|
|
|
1262
1286
|
message: z.string().describe("The cause: the API's own message, or what went wrong."),
|
|
1263
1287
|
});
|
|
1264
1288
|
const NOTES = z.array(z.string()).describe("Caveats, one sentence each.");
|
|
1289
|
+
// The success states (#2465). 2.1.0 described not_assessed as no dated measurement whose numbers
|
|
1290
|
+
// were all denied the status of findings, on the same schema as a cve_exposure answer relaying
|
|
1291
|
+
// services on record for a CISA-KEV CVE, which is not_assessed because its count is undated
|
|
1292
|
+
// (#2439). A client that respects the schema was told that count is not a finding. The count is
|
|
1293
|
+
// what the radar holds on record: not a dated measurement, and not nothing. So the description
|
|
1294
|
+
// says both, and leaves what each count is to the notes and exposure_state, which say it per
|
|
1295
|
+
// answer; it makes no blanket claim about the numbers, in either direction.
|
|
1296
|
+
const SUCCESS_STATE = "measured: a measurement of what was asked; an exposure count is measured only with measured_at and method. not_assessed: the answer holds no dated measurement of what was asked, so no count in it is presented as one; it can still relay a count, as what the source holds on record, undated, and its notes (and exposure_state, where the result carries it) say what each count is.";
|
|
1265
1297
|
function envelopeSchema(o) {
|
|
1266
1298
|
const coverage = o.coverage ? o.coverage.nullable() : z.null();
|
|
1267
1299
|
return z.discriminatedUnion("state", [
|
|
1268
1300
|
z.strictObject({
|
|
1269
|
-
state: z.enum(["measured", "not_assessed"]).describe(
|
|
1301
|
+
state: z.enum(["measured", "not_assessed"]).describe(SUCCESS_STATE),
|
|
1270
1302
|
measured_at: Instant.nullable().describe("When the underlying observation was made, as the API states it; null when the answer does not say or holds no observation."),
|
|
1271
1303
|
method: z.string().describe("How the numbers were produced."),
|
|
1272
1304
|
coverage: coverage.describe("What the answer covers; null where the answer says nothing about it."),
|
|
@@ -1373,7 +1405,7 @@ const CVE_EXPOSURE_OUTPUT = envelopeSchema({
|
|
|
1373
1405
|
exposure_state: z
|
|
1374
1406
|
.enum(["exposed", "measured_zero", "not_assessed", "tracking_unknown"])
|
|
1375
1407
|
.nullable()
|
|
1376
|
-
.describe("What the answer's count is: exposed, a
|
|
1408
|
+
.describe("What the answer's count is, as the note's (exposure_state: …) tag says; not state, which says whether the answer is a dated measurement. exposed: data.exposed_hosts counts the services the radar holds on record for the CVE, above 0. measured_zero: the API marks the CVE as tracked with 0 services, a zero in the radar's sample. not_assessed: the CVE is outside the radar's tracked set, so its count, if any, is not a current measurement. tracking_unknown: the API does not say whether the radar tracks the CVE, so its 0 is not evidence either way. null: the answer carries no count."),
|
|
1377
1409
|
},
|
|
1378
1410
|
});
|
|
1379
1411
|
// exposure_radar's data, from the readers' own specs, so the schema cannot drift from them.
|
|
@@ -1450,9 +1482,12 @@ const EXPOSURE_RADAR_OUTPUT = envelopeSchema({
|
|
|
1450
1482
|
const INSTRUCTIONS = [
|
|
1451
1483
|
"EchelonGraph's public CVE and internet-exposure data, read-only and keyless.",
|
|
1452
1484
|
"Everything these tools return is public: EchelonGraph's CVE Pulse feed, and aggregate, host-redacted totals from its exposure radars.",
|
|
1453
|
-
"Every result says how it was measured: state, measured_at, method, coverage, freshness and notes, in its structuredContent and again in its
|
|
1485
|
+
"Every result says how it was measured: state, measured_at, method, coverage, freshness and notes, in its structuredContent and again in its text.",
|
|
1486
|
+
"Its last text block is that structuredContent as JSON, less what an earlier text block already gives verbatim: data, which is a success's first text block; the sentences of the text block just before it (the note, or a failure's message), with which structuredContent's notes end; and method, where that block quotes it.",
|
|
1454
1487
|
"state is measured, not_assessed, failed or invalid_input.",
|
|
1455
|
-
"not_assessed means the answer holds no dated measurement of what was asked
|
|
1488
|
+
"not_assessed means the answer holds no dated measurement of what was asked, so no count in it is presented as one.",
|
|
1489
|
+
"It can still relay a count, as what the source holds on record, undated, and its notes say what each count is: cve_exposure's exposed_hosts when its exposure_state is exposed, and exposure_radar's labelled totals, are such counts.",
|
|
1490
|
+
"A zero is not a finding of no exposure where the answer holds no measurement for that CVE, for example a CVE outside the KEV-exposure radar's tracked set (exposure_state not_assessed).",
|
|
1456
1491
|
"failed and invalid_input mean nothing was measured: never report them as zero, none found or unexposed.",
|
|
1457
1492
|
"measured_at is when the underlying observation was made, and null when the answer does not say or holds none.",
|
|
1458
1493
|
"freshness gives the producing radar's last_run_at: when it last completed a check whose reads succeeded.",
|
|
@@ -1464,7 +1499,7 @@ const INSTRUCTIONS = [
|
|
|
1464
1499
|
const ANNOTATIONS = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
|
|
1465
1500
|
const CVE_ID_ARG = z.string().describe("a CVE ID, e.g. CVE-2023-44487");
|
|
1466
1501
|
// What each description says about its structured result, naming only fields its schema holds.
|
|
1467
|
-
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.";
|
|
1502
|
+
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.";
|
|
1468
1503
|
// One server, built per connection by serveStdio for whichever era the client opens with.
|
|
1469
1504
|
// tools/list answers in registration order, which is the order below.
|
|
1470
1505
|
function createServer() {
|
|
@@ -1497,14 +1532,14 @@ function createServer() {
|
|
|
1497
1532
|
}, async ({ cve_id }) => checked("get_cve", GET_CVE_OUTPUT, await getCVE(cve_id)));
|
|
1498
1533
|
server.registerTool("cve_exposure", {
|
|
1499
1534
|
title: "Internet exposure for one CVE",
|
|
1500
|
-
description: `${CVE_EXPOSURE_DESCRIPTION} Its structured result's state is not_assessed with measured_at null: the per-CVE answer says when EchelonGraph last wrote a row (last_seen), not when any counted service was observed, so no count is presented as a dated measurement; the count is still relayed, labelled. exposure_state says what the count is: exposed, measured_zero, not_assessed or tracking_unknown; coverage.in_scope is the API's tracked verdict; freshness is null, since the per-CVE answer carries no last completed check; data is the API's JSON. The result's last text block repeats the structured result without data.`,
|
|
1535
|
+
description: `${CVE_EXPOSURE_DESCRIPTION} Its structured result's state is not_assessed with measured_at null: the per-CVE answer says when EchelonGraph last wrote a row (last_seen), not when any counted service was observed, so no count is presented as a dated measurement; the count is still relayed, labelled, as what the radar holds on record. exposure_state says what the count is: exposed, measured_zero, not_assessed or tracking_unknown; coverage.in_scope is the API's tracked verdict; freshness is null, since the per-CVE answer carries no last completed check; data is the API's JSON. The result's last text block repeats the structured result without data (the first text block), without the note's sentences (the text block before it), with which notes ends, and without method where the note quotes it verbatim ("Method: …").`,
|
|
1501
1536
|
inputSchema: z.object({ cve_id: CVE_ID_ARG }),
|
|
1502
1537
|
outputSchema: CVE_EXPOSURE_OUTPUT,
|
|
1503
1538
|
annotations: ANNOTATIONS,
|
|
1504
1539
|
}, async ({ cve_id }) => checked("cve_exposure", CVE_EXPOSURE_OUTPUT, await cveExposure(cve_id)));
|
|
1505
1540
|
server.registerTool("exposure_radar", {
|
|
1506
1541
|
title: "Exposure radar totals",
|
|
1507
|
-
description: `${EXPOSURE_RADAR_DESCRIPTION} Its structured result's state is not_assessed with measured_at null: every radar answered, but no stats answer says when the services or records it counts were observed, so no count is presented as a dated measurement. freshness gives each radar's last_run_at (and shadow_ai's running) where the API serves one; coverage names the radars that answered; data is the relayed result above. The result's last text block repeats the structured result without data.`,
|
|
1542
|
+
description: `${EXPOSURE_RADAR_DESCRIPTION} Its structured result's state is not_assessed with measured_at null: every radar answered, but no stats answer says when the services or records it counts were observed, so no count is presented as a dated measurement; each count is still relayed, labelled, as what that radar holds on record. freshness gives each radar's last_run_at (and shadow_ai's running) where the API serves one; coverage names the radars that answered; data is the relayed result above. The result's last text block repeats the structured result without data (the first text block) and without the note's sentences (the text block before it), with which notes ends.`,
|
|
1508
1543
|
outputSchema: EXPOSURE_RADAR_OUTPUT,
|
|
1509
1544
|
annotations: ANNOTATIONS,
|
|
1510
1545
|
}, async () => checked("exposure_radar", EXPOSURE_RADAR_OUTPUT, await exposureRadar()));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "echelongraph-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
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.
|
|
11
|
+
"version": "2.2.0",
|
|
12
12
|
"packages": [
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "echelongraph-mcp",
|
|
16
|
-
"version": "2.
|
|
16
|
+
"version": "2.2.0",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
},
|