echelongraph-mcp 1.0.3 → 2.0.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/LICENSE +21 -0
- package/README.md +167 -10
- package/dist/index.js +893 -80
- package/package.json +9 -6
- package/server.json +6 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EchelonGraph
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -19,9 +19,9 @@ API call a tool needs to answer.
|
|
|
19
19
|
|---|---|
|
|
20
20
|
| `cve_summary` | Counts of active CVEs by severity, and when the feed was last updated. |
|
|
21
21
|
| `search_cves` | Search/filter CVEs (severity, min CVSS, text, sort) with EchelonGraph scores. |
|
|
22
|
-
| `get_cve` | Full
|
|
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. |
|
|
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
|
-
| `exposure_radar` | Aggregate totals across the exposure radars:
|
|
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. 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.
|
|
@@ -40,8 +40,9 @@ Shodan InternetDB refreshes `last_seen` without re-reading the banner, so a patc
|
|
|
40
40
|
stay counted while its port stays open.
|
|
41
41
|
|
|
42
42
|
The unit is a service, not a machine. Shodan returns one banner per port and the radar keys
|
|
43
|
-
each observation on ip:port, so a machine answering on two ports counts twice. The API field
|
|
44
|
-
|
|
43
|
+
each observation on ip:port, so a machine answering on two ports counts twice. The API's field
|
|
44
|
+
names are kept (`exposed_hosts` here; `distinct_hosts`, `ransomware_hosts` and each ranked
|
|
45
|
+
row's count in `exposure_radar`), but every one of them counts ip:port services.
|
|
45
46
|
|
|
46
47
|
The radar only looks for its **tracked** set of CVEs, so a zero means different things:
|
|
47
48
|
|
|
@@ -52,7 +53,79 @@ The radar only looks for its **tracked** set of CVEs, so a zero means different
|
|
|
52
53
|
| no `tracked` field | The API did not say whether the CVE is in the tracked set: an older API, or the radar cannot decide (for example a CISA-KEV or high-EPSS CVE in a tracked product with 0 services on record). 0 exposed services on record, with no claim either way. |
|
|
53
54
|
| HTTP 400, or an id that is not `CVE-YYYY-NNNN…` | An error result tagged `invalid_input`; nothing was looked up. |
|
|
54
55
|
|
|
55
|
-
### How `exposure_radar` counts
|
|
56
|
+
### How `exposure_radar` counts
|
|
57
|
+
|
|
58
|
+
`exposure_radar` relays each radar's answer cut to the fields it can label, and its note labels
|
|
59
|
+
every number by what it counts. A field this version does not know is left out and the note
|
|
60
|
+
names it, whether it is a total or a field inside a ranked row; so is a known field in an
|
|
61
|
+
unexpected shape, and a list with one malformed row is left out whole, since a partial list
|
|
62
|
+
reads as a complete one. `kev_exposure`, `exposed_databases` and `leaked_credentials` also
|
|
63
|
+
carry `generated_at`, when the API computed their totals, and, when the API can tell,
|
|
64
|
+
`last_run_at`: when that radar last completed a check (each table below says what a completed
|
|
65
|
+
check is for that radar).
|
|
66
|
+
|
|
67
|
+
`last_run_at` is a timestamp, not a count. It is recorded for the radar as a whole, not for
|
|
68
|
+
the server instance that answered, in UTC to the second. It moves only at the end of a cycle
|
|
69
|
+
whose reads succeeded, so a cycle that read nothing leaves it where it was. It is not the time
|
|
70
|
+
of every record a radar's numbers count: those cover everything still on record, not only what
|
|
71
|
+
the last check found. Nor is it `generated_at`, which is only when the API recomputed the
|
|
72
|
+
totals. The API omits `last_run_at` when it has no completed check on record or cannot read
|
|
73
|
+
it; the result then carries none, and the note says nothing about it. When it is there, the
|
|
74
|
+
note gives it as, for example, "kev_exposure last completed check: 2026-09-27T03:12:44Z".
|
|
75
|
+
|
|
76
|
+
#### `kev_exposure`
|
|
77
|
+
|
|
78
|
+
Every count is over the services the KEV-exposure radar keeps: a Shodan banner whose version
|
|
79
|
+
maps to a CISA-KEV-listed CVE (see "How `cve_exposure` counts").
|
|
80
|
+
|
|
81
|
+
| Field | What it counts |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `kev_exposure.distinct_hosts` | Distinct ip:port services, not machines, with at least one CISA-KEV-listed CVE on record. A machine answering on two ports counts twice. |
|
|
84
|
+
| `kev_exposure.ransomware_hosts` | The services among them with at least one ransomware-linked KEV CVE (one whose KEV entry notes known use in ransomware campaigns). |
|
|
85
|
+
| `kev_exposure.kev_cves_exposed` | Distinct CISA-KEV-listed CVEs with at least one service on record. |
|
|
86
|
+
| `kev_exposure.ransomware_cves` | The ransomware-linked CVEs among them. |
|
|
87
|
+
| `kev_exposure.correlations` | Service×CVE pairs, not services: a service with three KEV CVEs counts three times. |
|
|
88
|
+
| `kev_exposure.top_products`, `kev_exposure.top_countries`, `kev_exposure.top_cves` | Up to 12 products, 10 countries and 12 CVEs, ranked by distinct ip:port services. |
|
|
89
|
+
| `kev_exposure.top_cves[].cvss_v3_score`, `kev_exposure.top_cves[].epss_score` | Scores, not counts: the highest CVSS v3 base score and EPSS probability (0 to 1) recorded on that CVE's observations. |
|
|
90
|
+
| `kev_exposure.trend` | Service×CVE pairs, not services, by the week (starting Monday) in which each pair was first recorded, over the last 12 weeks. Only pairs still on record are counted, so earlier weeks read low. |
|
|
91
|
+
| `kev_exposure.newest_kev` | The 15 CVEs that EchelonGraph's CVE records most recently mark as CISA-KEV-listed (by `added_date`), each with an `exposure_state` (below). |
|
|
92
|
+
| `kev_exposure.newest_kev[].exposed_hosts` | Only on a row whose `exposure_state` is `exposed` (or `measured_zero`): distinct ip:port services on record with that CVE. |
|
|
93
|
+
| `kev_exposure.newest_kev[].cvss_v3_score`, `kev_exposure.newest_kev[].epss_score` | Scores, not counts: the CVE record's CVSS v3 base score and EPSS probability (0 to 1), absent when the record has none. |
|
|
94
|
+
| `kev_exposure.last_run_at` | A timestamp, not a count: when the radar last completed a check, a cycle in which its Shodan search answered at least one query (others may have failed) and its list of services due for a re-check was read. A cycle that skipped the search for want of Shodan query credits, or whose reads failed, does not move it. |
|
|
95
|
+
|
|
96
|
+
A `newest_kev` row's `exposure_state` says whether its count is a measurement:
|
|
97
|
+
|
|
98
|
+
| `exposure_state` | What it means |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `exposed` | The radar has services on record with this CVE, and `exposed_hosts` counts them. |
|
|
101
|
+
| `not_assessed` | **NOT ASSESSED**: the answer holds no measurement for this CVE, so no count is relayed. The API answers 0 for every CVE the radar holds no service for, whether or not the radar looks for that CVE at all, and it never marks a CVE as tracked with 0 services, so that 0 is not a measurement. `cve_exposure` says per CVE whether it is in the radar's tracked set. |
|
|
102
|
+
| `measured_zero` | Only if the API ever marks a row `tracked: true` with 0 services: a zero in the radar's sample, not an internet-wide zero. If it marks a row `tracked: false`, that row is `not_assessed` whatever its count. |
|
|
103
|
+
|
|
104
|
+
#### `exposed_databases`
|
|
105
|
+
|
|
106
|
+
| Field | What it counts |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `exposed_databases.distinct_hosts` | Distinct ip:port services, not machines, that EchelonGraph's check confirmed answering without authentication: data stores and observability UIs alike. |
|
|
109
|
+
| `exposed_databases.engines` | Distinct engine types among them (for example `redis` or `grafana`). |
|
|
110
|
+
| `exposed_databases.top_engines`, `exposed_databases.top_countries` | Up to 15 engines and 10 countries, ranked by those services. |
|
|
111
|
+
| `exposed_databases.pii_likely`, `exposed_databases.pci_likely` | Services whose schema names pass a high-confidence gate for personal data, or for payment-card data. The names are index, database, table or field names in the Shodan or LeakIX banner or in the check's response, never record values; the gate passes one unambiguous term (such as `ssn` or `cardholder`) or two distinct indicators. It is a precision-first schema gate, not a census: a service whose names do not pass it is not counted, whatever it holds, so the other services are not shown to hold no such data. |
|
|
112
|
+
| `exposed_databases.last_run_at` | A timestamp, not a count: when the radar last completed a check, a cycle in which its Shodan search, or the LeakIX fallback, answered at least one query (others may have failed), its list of services due for a re-check was read, and the scan opt-out register could be consulted. A cycle that searched nothing, or whose reads failed, does not move it. |
|
|
113
|
+
|
|
114
|
+
#### `leaked_credentials`
|
|
115
|
+
|
|
116
|
+
| Field | What it counts |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `leaked_credentials.total` | (repository, secret) pairs, not distinct secrets: a secret committed to three repositories counts three times. |
|
|
119
|
+
| `leaked_credentials.distinct_secrets` | Each secret once. |
|
|
120
|
+
| `leaked_credentials.distinct_repos` | Public GitHub repositories with at least one. |
|
|
121
|
+
| `leaked_credentials.top_providers`, `leaked_credentials.top_types` | Up to 15 providers and secret types, ranked by (repository, secret) pairs. |
|
|
122
|
+
| `leaked_credentials.last_run_at` | A timestamp, not a count: when the radar last completed a check, a cycle that read the public GitHub event stream (fetches of some of the commits it lists may have failed). A cycle whose read of that stream failed does not move it. |
|
|
123
|
+
|
|
124
|
+
None of them is validated. Each is a credential-shaped string in a public commit that passed
|
|
125
|
+
EchelonGraph's filters, at most structurally checked (a checksum or format decode), and never
|
|
126
|
+
tested against its provider, so none of these is a count of working credentials.
|
|
127
|
+
|
|
128
|
+
#### `shadow_ai`
|
|
56
129
|
|
|
57
130
|
The shadow-AI radar's answer mixes numbers that count exposed services with numbers that count
|
|
58
131
|
every observation. So `exposure_radar` regroups it by what each number counts, and its note
|
|
@@ -78,7 +151,7 @@ yet verified, whose hostname no longer resolves, or whose service no longer answ
|
|
|
78
151
|
|
|
79
152
|
The tool relays no number it cannot label. A stats field this version does not know is left
|
|
80
153
|
out, and the note names it; so is a count in an unexpected shape. A ranked row carries only
|
|
81
|
-
its name and its count.
|
|
154
|
+
its name and its count, and a field added to one is named too.
|
|
82
155
|
|
|
83
156
|
## What a result means
|
|
84
157
|
|
|
@@ -89,8 +162,9 @@ outage for an all-clear:
|
|
|
89
162
|
note saying the call succeeded, which base URL answered, and what it found. When the
|
|
90
163
|
feed genuinely holds nothing for the query the note says so in words ("we looked and
|
|
91
164
|
found nothing … not a lookup failure"), because a measured zero is a measurement. The
|
|
92
|
-
exception to verbatim is `exposure_radar
|
|
93
|
-
|
|
165
|
+
exception to verbatim is `exposure_radar`: each radar is cut to the fields listed above, and
|
|
166
|
+
each `kev_exposure.newest_kev` row gains an `exposure_state`. Its `shadow_ai` counts are
|
|
167
|
+
regrouped as above, and its `poller` block carries only `running` and `last_run_at`, or is dropped when it
|
|
94
168
|
carries no real completion time. The block's other fields describe the server instance that
|
|
95
169
|
answered, not the radar. Its `last_run_at` is when the radar's leader last
|
|
96
170
|
completed a Certificate Transparency (crt.sh) cycle, and its `running` is true only when
|
|
@@ -107,9 +181,87 @@ outage for an all-clear:
|
|
|
107
181
|
cause (status code or error kind), the path, and the base URL, and says it is not a
|
|
108
182
|
finding. A failure is never rendered as a success with null fields.
|
|
109
183
|
|
|
184
|
+
Both shapes also carry a structured result, below. The text blocks are the same as in 1.x, so
|
|
185
|
+
a client that reads only `content` sees what it always saw.
|
|
186
|
+
|
|
187
|
+
## Structured results
|
|
188
|
+
|
|
189
|
+
Since 2.0.0 every result, success or failure, carries `structuredContent`, and every tool
|
|
190
|
+
declares its shape as an `outputSchema` in `tools/list`, with a title and the annotations
|
|
191
|
+
`readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true` and
|
|
192
|
+
`openWorldHint: true` (hints, which a client treats as untrusted).
|
|
193
|
+
|
|
194
|
+
| Field | What it holds |
|
|
195
|
+
|---|---|
|
|
196
|
+
| `state` | `measured`, `not_assessed`, `failed` or `invalid_input` (below). |
|
|
197
|
+
| `measured_at` | When the underlying observation was made, as the API states it. `null` when the answer does not say or holds no observation; never the Go zero time. |
|
|
198
|
+
| `method` | How the numbers were produced. `null` on a failure. |
|
|
199
|
+
| `coverage` | What the answer covers, where the tool can say: `in_scope` for `cve_exposure`, the list's own account of its count for `search_cves`, and the radars that answered for `exposure_radar`. |
|
|
200
|
+
| `freshness` | The producing radar's last completed check (`last_run_at`), where the API serves one; `null` where it serves none. |
|
|
201
|
+
| `notes` | The caveats, one sentence each: what the envelope itself needs saying, then the note from the text block. |
|
|
202
|
+
| `data` | On a success only: the same JSON as the first text block. |
|
|
203
|
+
| `error` | On a failure only: `kind`, `path`, `status` and `message`. |
|
|
204
|
+
|
|
205
|
+
| `state` | What it means |
|
|
206
|
+
|---|---|
|
|
207
|
+
| `measured` | A measurement of what was asked. An exposure count is `measured` only when the answer dates it (`measured_at`) and says how it was produced (`method`). |
|
|
208
|
+
| `not_assessed` | The answer holds no dated measurement of what was asked, so no number in it is a finding: 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. |
|
|
209
|
+
| `failed` | The lookup did not complete. Not a finding. |
|
|
210
|
+
| `invalid_input` | The input was refused, by this server or by the API, so nothing was looked up. Not a finding. |
|
|
211
|
+
|
|
212
|
+
Every field comes from what the API sends; where the API does not say, the field is `null` and
|
|
213
|
+
a note says so. Per tool:
|
|
214
|
+
|
|
215
|
+
- `cve_summary` is `measured`, and its `measured_at` is `summary.last_updated`, the newest
|
|
216
|
+
modification time among the active CVE records it counts.
|
|
217
|
+
- `search_cves` is `measured`, with `measured_at` `null`: each record carries its own times. Its
|
|
218
|
+
`coverage` repeats the list's `total`, `total_counted`, `total_is_lower_bound`,
|
|
219
|
+
`search_relaxed`, `limit` and `offset`, and adds `returned`, the rows in the page. When
|
|
220
|
+
`total_counted` is false the total is not a count, and the note does not call it one.
|
|
221
|
+
- `get_cve` is `measured`, and its `measured_at` is the record's `updated_at`, when EchelonGraph
|
|
222
|
+
last wrote it.
|
|
223
|
+
- `cve_exposure` is `measured` only for services on record with a real `last_seen`, which is its
|
|
224
|
+
`measured_at`. Every other answer is `not_assessed`, a tracked zero included, since a zero has
|
|
225
|
+
no observation to date it. `exposure_state` is `exposed`, `measured_zero`, `not_assessed` or
|
|
226
|
+
`tracking_unknown`, the cases of "How `cve_exposure` counts", and `coverage.in_scope` is the
|
|
227
|
+
API's `tracked`.
|
|
228
|
+
- `exposure_radar` is `not_assessed` with `measured_at` `null`: every radar answered, but no
|
|
229
|
+
stats answer says when the services or records it counts were observed, so no count is
|
|
230
|
+
presented as a dated measurement. Its numbers keep the labels above. `freshness` holds each
|
|
231
|
+
radar's `last_run_at` where the API serves one, and for `shadow_ai` also `running`.
|
|
232
|
+
|
|
233
|
+
The CVE feed tools' `freshness` is `null`: the feed's answers carry no time at which its
|
|
234
|
+
pollers last completed a poll for the feed as a whole.
|
|
235
|
+
|
|
236
|
+
A success whose fields do not fit the tool's `outputSchema` (a field of a type the schema does
|
|
237
|
+
not allow) is returned as a failure with `error.kind` `unexpected_shape`, never relayed. A
|
|
238
|
+
field the API adds later is still relayed by the four tools that relay the API's JSON, and
|
|
239
|
+
`exposure_radar` leaves it out and names it, as above.
|
|
240
|
+
|
|
241
|
+
## Protocol versions
|
|
242
|
+
|
|
243
|
+
The server answers both eras of the Model Context Protocol on stdio:
|
|
244
|
+
|
|
245
|
+
- **2026-07-28**: a client that opens with `server/discover` receives a DiscoverResult listing
|
|
246
|
+
`2026-07-28`, the tools capability and the server instructions, and then sends each request
|
|
247
|
+
with the per-request `_meta` envelope.
|
|
248
|
+
- **2025 and earlier**: a client that opens with `initialize`, as every 1.x SDK client does,
|
|
249
|
+
negotiates `2025-11-25`, `2025-06-18`, `2025-03-26` or `2024-11-05`; a version the server does
|
|
250
|
+
not know is answered with `2025-11-25`.
|
|
251
|
+
|
|
252
|
+
The first message of a connection picks its era. The DiscoverResult lists only `2026-07-28`,
|
|
253
|
+
as the SDK builds it: the 2025-era versions are reached through `initialize`. Both handshakes
|
|
254
|
+
carry the package's name and version (`serverInfo`), and every API request carries them in its
|
|
255
|
+
User-Agent, `echelongraph-mcp/<version>`.
|
|
256
|
+
|
|
257
|
+
Both handshakes also carry the server instructions: the data is public; what `state`,
|
|
258
|
+
`measured_at` and `freshness` mean; that exposure numbers are aggregate counts of ip:port
|
|
259
|
+
services, not an internet-wide census; and that Shodan data is Shodan's.
|
|
260
|
+
|
|
110
261
|
## Install
|
|
111
262
|
|
|
112
|
-
Add it to your MCP client's config. It runs via `npx` — no global
|
|
263
|
+
Requires Node.js 20 or later. Add it to your MCP client's config. It runs via `npx` — no global
|
|
264
|
+
install needed.
|
|
113
265
|
|
|
114
266
|
### Claude Desktop
|
|
115
267
|
|
|
@@ -156,13 +308,18 @@ services does EchelonGraph's radar have on record for it?"*
|
|
|
156
308
|
```bash
|
|
157
309
|
npm install
|
|
158
310
|
npm run build # tsc → dist/
|
|
159
|
-
npm test # build, then behavioural
|
|
311
|
+
npm test # build, then the behavioural suite over both protocol eras, against a stub API (no network needed)
|
|
160
312
|
npm run smoke # spawn the server + call cve_exposure against the production API
|
|
161
313
|
```
|
|
162
314
|
|
|
163
315
|
`npm run smoke` calls the production API with this package's User-Agent, so its requests are
|
|
164
316
|
counted as external MCP adoption. `npm test` never leaves the machine.
|
|
165
317
|
|
|
318
|
+
`npm test` runs the suite once over a 2026-07-28 `server/discover` and once over a 2025-06-18
|
|
319
|
+
`initialize`, plus the era tests. To run it against an installed package rather than `dist/`,
|
|
320
|
+
set `ECHELONGRAPH_MCP_BIN` to that package's `echelongraph-mcp` bin: the tests then run the bin
|
|
321
|
+
directly, as `npx` does, and read that package's own files.
|
|
322
|
+
|
|
166
323
|
## License
|
|
167
324
|
|
|
168
325
|
The code is MIT-licensed.
|