
EchelonGraph CVE & Exposure
io.echelongraphv2.3.4Updated Oct 3, 2026
CVEs (NVD, CISA-KEV, EPSS, GHSA) and per-CVE exposure from Shodan data (© Shodan). Free, keyless.
Overview
Lets an assistant look up CVE severity, exploitation and internet-exposure data from EchelonGraph's free public feed.
- What it does
- Provides five read-only tools over EchelonGraph's CVE and exposure data: cve_summary for counts of active CVEs by severity band, search_cves for filtering CVEs by severity, CVSS, text and sort, get_cve for one CVE's full record (CVSS v3/v4, EPSS, CISA-KEV status, ransomware use, GHSA id, references), cve_exposure for a CVE's internet-exposure footprint, and exposure_radar for aggregate totals such as KEV exposure, unauthenticated data stores, leaked credentials, shadow AI and MCP servers. Results relay the API's JSON with a note explaining what each number counts.
- When to use it
- Useful when an assistant needs to triage vulnerabilities: checking whether a CVE is actively exploited or KEV-listed, comparing severity and EPSS scores, or gauging how many internet-facing services are on record for a CVE. Also for aggregate exposure and leaked-credential statistics.
- Requirements
- Node.js 20 or later; runs locally via npx with no global install. No API key or authentication. Optional environment variables ECHELONGRAPH_API_BASE (default and ECHELONGRAPH_API_TIMEOUT_MS (default 15000). Network access to the EchelonGraph API is required.
Installation
In SourceWeft
- Open EchelonGraph CVE & Exposure in the dashboard and add it to a workspace.
- Enable the server for the chats that should use its tools.
Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.
Other MCP clients
Follow the launch instructions in the repository.
README
EchelonGraph MCP server
CVE and internet-exposure data for Claude, Cursor, Cline, and any MCP client, straight from EchelonGraph's free public feed.
It exposes EchelonGraph's CVE Pulse (NVD + MITRE-CNA pre-NVD + CISA-KEV + EPSS + GitHub GHSA, fused into one score) plus a per-CVE internet-exposure footprint: how many internet-facing services (distinct ip:port) EchelonGraph's KEV-exposure radar has on record running a version that maps to the CVE. Exposure counts are derived from Shodan data. Shodan data is owned by Shodan, which holds its copyright (© Shodan).
Free and keyless: no API key, no auth, read-only. The server makes no request other than the API call a tool needs to answer.
Tools
Every figure is as fresh as the schedule that refreshes it. The CVE feed is polled from its sources on a schedule; each radar refreshes on its own.
How cve_summary reads its counts
summary.critical, summary.high, summary.medium and summary.low count the active CVEs by
severity band, and summary.total counts them all. summary.none counts the active CVEs with no
severity band from any source: CVEs not yet scored, not CVEs rated None. The answer sends the same
count again as summary.unscored.
summary.nvd_critical, summary.nvd_high, summary.nvd_medium, summary.nvd_low and
summary.nvd_none count the same active CVEs a second time, by NVD's CVSS severity label (v3.x,
else v4.0). Before NVD's record arrives, or where it gives none, a pre-NVD label from the CVE.org
record or a GitHub advisory can stand in. They are provenance, never EchelonGraph's severity band. summary.nvd_none counts the active CVEs with no
Critical, High, Medium or Low label there, and many of those carry an NVD CVSS v2 score instead, so
it is neither a count of CVEs rated None nor the count of CVEs with no severity, which is
summary.none.
summary.rejected counts the CVE records rejected (withdrawn) by their numbering authority.
summary.total and the other counts leave them out, and they are withdrawn records, never
vulnerabilities.
The tool relays the API's JSON as it was sent. When summary.none, summary.nvd_none or
summary.rejected is above zero, the note says what it counts, with the number from the answer.
The note says the five NVD counts add up to summary.total only when the answer's do, and says
nothing about a field the answer does not carry.
How get_cve and search_cves read the EchelonGraph score
echelongraph_score (0 to 10), its band echelongraph_severity and the risk priority
echelongraph_risk (0 to 100) are EchelonGraph's score only when the record's
score_assessed is true. A CVE EchelonGraph has not scored carries score_assessed: false,
and it is not yet scored, not scored 0. The API leaves those three fields out on it, or (an
API before that change) sends 0, NONE and 0 as placeholders, which are not a rating and do
not mean the CVE is harmless. Its score_confidence is NONE, and score_unassessed_reason
says why: no source has yet published severity data EchelonGraph can score, or the record was
rejected (withdrawn) by its numbering authority, and a rejected record is never scored.
Both tools relay the API's JSON as it was sent, and the note says what the score is, for
get_cve per CVE and for search_cves per row, naming each CVE it is about:
How cve_exposure counts
Every 12 hours, when Shodan query credits allow, the KEV-exposure 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 (>= 0.5) CVE. A service whose row has not been written or refreshed for 21 days is dropped. A count is therefore a banner-version inference over a sample: not an exploit test, and not an internet-wide census.
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. Shodan's own time for the banner is not stored. So
last_seen dates the write, never the sighting, and it is never measured_at (see
"Structured results").
The unit is a service, not a machine. Shodan returns one banner per port and the radar keys
each observation on ip:port, so a machine answering on two ports counts twice. The API's field
names are kept (exposed_hosts here; distinct_hosts, ransomware_hosts and each ranked
row's count in exposure_radar), but every one of them counts ip:port services.
The radar only looks for its tracked set of CVEs, so a zero means different things. The
note tags each case with its exposure_state, as "(exposure_state: …)"; that tag says what the
count is, and is not the result's state:
Whatever the exposure_state, the result's state is not_assessed with measured_at null:
the answer does not say when any service it counts was observed (see "Structured results").
How exposure_radar counts
exposure_radar relays each radar's answer cut to the fields it can label, and its note labels
every number by what it counts. A field this version does not know is left out and the note
names it, whether it is a total or a field inside a ranked row; so is a known field in an
unexpected shape, and a list with one malformed row is left out whole, since a partial list
reads as a complete one. kev_exposure, exposed_databases and leaked_credentials also
carry generated_at, when the API computed their totals, and, when the API can tell,
last_run_at: when that radar last completed a check (each table below says what a completed
check is for that radar).
last_run_at is a timestamp, not a count. It is recorded for the radar as a whole, not for
the server instance that answered, in UTC to the second. It moves only at the end of a cycle
whose reads succeeded, so a cycle that read nothing leaves it where it was. It is not the time
of every record a radar's numbers count: those cover everything still on record, not only what
the last check found. Nor is it generated_at, which is only when the API recomputed the
totals. The API omits last_run_at when it has no completed check on record or cannot read
it; the result then carries none, and the note says nothing about it. When it is there, the
note gives it as, for example, "kev_exposure last completed check: 2026-09-27T03:12:44Z".
kev_exposure
Every count is over the services the KEV-exposure radar keeps: a Shodan banner whose version
maps to a CISA-KEV-listed CVE (see "How cve_exposure counts").
A newest_kev row's exposure_state says whether its count is a measurement:
exposed_databases
leaked_credentials
None of them is validated. Each is a credential-shaped string in a public commit that passed EchelonGraph's filters, at most structurally checked (a checksum or format decode), and never tested against its provider, so none of these is a count of working credentials.
shadow_ai
The shadow-AI radar's answer mixes numbers that count exposed services with numbers that count
every observation. So exposure_radar regroups it by what each number counts, and its note
labels each one. Only shadow_ai.confirmed_exposed counts exposed services.
Neither authentication count is part of confirmed_exposed, and observed.total minus
confirmed_exposed.total is not a count of secured services: it also holds observations not
yet verified, whose hostname no longer resolves, or whose service no longer answers openly.
The tool relays no number it cannot label. A stats field this version does not know is left
out, and the note names it; so is a count in an unexpected shape. A ranked row carries only
its name and its count, and a field added to one is named too. The poller block's keys are
checked the same way: a key this version does not know is left out and named, while the
instance fields it knows are left out as described below.
mcp_servers
The AI-exposure radar's MCP-server verdicts, from GET /api/v1/public/ai-exposure/stats?service=mcp.
Every hostname was named like an MCP server in EchelonGraph's own Certificate Transparency
feed, matched by hostname pattern; no Shodan data is used. Each is checked by EchelonGraph's
identified MCP probe: a server/discover request, and initialize only if that is refused. It
never sends tools/call. The answer is counts and timestamps only, with no hostname, version or
server name, and the tool repeats no string it carries but its timestamps. Each hostname is
counted once, by its latest verdict on record, and EchelonGraph's own control servers are left
out. The unit is a hostname, not an ip:port service.
mcp_servers.not_assessed_by_reason divides mcp_servers.not_assessed:
The eight credential-challenge buckets hold endpoints that asked for credentials: only their metadata did not validate, so none of them is shown to lack protection.
mcp_servers.era and mcp_servers.transport each divide every counted hostname, so each adds
up to mcp_servers.total:
The tool refuses the answer, as a failure of that radar and never as zeros, when it does not say
service mcp (an API older than ?service=mcp ignores the parameter and answers its counts
over every AI service it checks), when mcp_servers.total, mcp_servers.protected,
mcp_servers.pending_readjudication or mcp_servers.not_assessed is missing or not a whole
number, or when they contradict each other. A partition whose buckets are not exactly those
above, each a whole number adding up to the count it divides, is left out whole and named. A
field this version does not know is left out, and named only when its name is shaped like a
field name.
What a result means
Every tool answers in one of two shapes, so a model reading the result cannot mistake an outage for an all-clear:
- Success — the first text block is the API's JSON verbatim; the second is a one-line
note saying the call succeeded, which base URL answered, and what it found; the third is
the structured result as JSON, less what the first two already say (below). When the
feed genuinely holds nothing for the query the note says so in words ("we looked and
found nothing … not a lookup failure"), because a measured zero is a measurement. The
exception to verbatim is
exposure_radar: each radar is cut to the fields listed above, and eachkev_exposure.newest_kevrow gains anexposure_state. Itsshadow_aicounts are regrouped as above, and itspollerblock carries onlyrunningandlast_run_at, or is dropped when it carries no real completion time. The block's other fields describe the server instance that answered, not the radar. Itslast_run_atis when the radar's leader last completed a Certificate Transparency (crt.sh) cycle, and itsrunningis true only when that was within 30 minutes of the answer. Whenrunningis true, the note says the radar's leader last completed a cycle at that time; when it is false, the note says no cycle has completed since that time; whenlast_run_atis absent, the note says the radar's freshness is unknown and the block is left out. An older API answered from whichever server instance served the request, and a follower instance sentrunning: falsewith the zero time0001-01-01T00:00:00Z; that too is unknown freshness, never presented as a stopped radar. - Failure — an MCP error result (
isError: true) whenever the lookup did not complete: the host could not be reached, it answered non-2xx, it took longer than the timeout, or it answered 2xx with a body that is not a JSON object. The text names the tool, the cause (status code or error kind), the path, and the base URL, and says it is not a finding; the second text block is the structured result as JSON, less the message's sentences (below). A failure is never rendered as a success with null fields.
Both shapes also carry a structured result, below, and repeat it in their last text block, less
what an earlier block already says, so a client that passes only content to the model still
sees how the answer was measured. The first text block (the API's JSON, or the failure) and the
note after it are where 1.x put them.
Structured results
Since 2.0.0 every result, success or failure, carries structuredContent, and every tool
declares its shape as an outputSchema in tools/list, with a title and the annotations
readOnlyHint: true, destructiveHint: false, idempotentHint: true and
openWorldHint: true (hints, which a client treats as untrusted).
The last text block of every result is this structured result serialized as JSON, less what
an earlier text block already says verbatim, since a client may pass only content to the
model. It carries the same state, measured_at, coverage and freshness (and on a failure
error), key for key. It leaves out data, which is a success's first text block, and the
note's sentences, which are the text block just before it (on a failure, the message) and
with which notes ends: its notes holds only the sentences the envelope adds about itself,
and is left out when there are none. It leaves out method too where that note quotes it verbatim, as
cve_exposure's does ("Method: …"). So the text blocks together carry the whole structured
result, and cannot disagree with it. Before 2.2.0 this block repeated the whole note, so every
note was sent twice.
Every field comes from what the API sends; where the API does not say, the field is null and
a note says so. Per tool:
cve_summaryismeasured, and itsmeasured_atissummary.last_updated, the newest modification time among the active CVE records it counts.search_cvesismeasured, withmeasured_atnull: each record carries its own times. Itscoveragerepeats the list'stotal,total_counted,total_is_lower_bound,search_relaxed,limitandoffset, and addsreturned, the rows in the page. Whentotal_countedis false the total is not a count, and the note does not call it one.get_cveismeasured, and itsmeasured_atis the record'supdated_at, when EchelonGraph last wrote it.cve_exposureisnot_assessedwithmeasured_atnull, every answer: the per-CVE answer carries no time at which the services it counts were observed. Itslast_seenis when EchelonGraph last wrote or refreshed one of their rows, not when any of them was observed, so it is nevermeasured_at, and a note says so. A tracked zero isnot_assessedtoo, since a zero has no observation to date it. The count is still relayed, labelled:exposure_stateisexposed,measured_zero,not_assessedortracking_unknown, the cases of "Howcve_exposurecounts", andcoverage.in_scopeis the API'stracked. It becomesmeasuredonly when the API serves a time at which the counted services were observed.exposure_radarisnot_assessedwithmeasured_atnull: every radar answered, but no answer gives one time at which what it counts was observed, so no count is presented as a dated measurement.mcp_serversdates its verdicts only by a window,mcp_servers.window.fromtomcp_servers.window.to, over checks made at different times, which is not one observation time. Its numbers keep the labels above.freshnessholds each radar'slast_run_atwhere the API serves one, forshadow_aialsorunning, and formcp_serversalsoenabled.
The CVE feed tools' freshness is null: the feed's answers carry no time at which its
pollers last completed a poll for the feed as a whole.
A success whose fields do not fit the tool's outputSchema (a field of a type the schema does
not allow) is returned as a failure with error.kind unexpected_shape, never relayed. A
field the API adds later is still relayed by the four tools that relay the API's JSON, and
exposure_radar leaves it out and names it, as above.
Protocol versions
The server answers both eras of the Model Context Protocol on stdio:
- 2026-07-28: a client that opens with
server/discoverreceives a DiscoverResult listing2026-07-28, the tools capability and the server instructions, and then sends each request with the per-request_metaenvelope. - 2025 and earlier: a client that opens with
initialize, as every 1.x SDK client does, negotiates2025-11-25,2025-06-18,2025-03-26or2024-11-05; a version the server does not know is answered with2025-11-25.
The first message of a connection picks its era. The DiscoverResult lists only 2026-07-28,
as the SDK builds it: the 2025-era versions are reached through initialize. Both handshakes
carry the package's name and version (serverInfo), and every API request carries them in its
User-Agent, echelongraph-mcp/<version>.
Both handshakes also carry the server instructions: the data is public; what state,
measured_at and freshness mean, and that the last text block repeats them; that exposure
numbers are aggregate counts of ip:port services, not an internet-wide census; and that Shodan
data is Shodan's.
Install
Requires Node.js 20 or later. Add it to your MCP client's config. It runs via npx — no global
install needed.
Claude Desktop
claude_desktop_config.json → mcpServers:
Cursor / Cline / Windsurf
~/.cursor/mcp.json (or the client's MCP settings):
Restart the client, then ask: "Is CVE-2023-44487 actively exploited, and how many exposed services does EchelonGraph's radar have on record for it?"
Configuration
Develop
npm run smoke calls the production API with this package's User-Agent, so its requests are
counted as external MCP adoption. npm test never leaves the machine.
npm test runs the suite once over a 2026-07-28 server/discover and once over a 2025-06-18
initialize, plus the era tests. To run it against an installed package rather than dist/,
set ECHELONGRAPH_MCP_BIN to that package's echelongraph-mcp bin: the tests then run the bin
directly, as npx does, and read that package's own files.
License
The code is MIT-licensed.
The CVE Pulse compilation (how EchelonGraph combines its CVE sources, and the EchelonGraph score) is © EchelonGraph, served under the CVE Pulse free-access terms; the source records it compiles stay under their publishers' terms.
Exposure counts are derived from Shodan data. Shodan data is owned by Shodan, which holds its copyright (© Shodan). EchelonGraph claims no ownership of it or copyright in it.
Source: README.md at commit 54c58a4
Tools
0Version history
1- v2.3.4LatestOct 3, 2026
