EchelonGraph MCP Server
OfficialEchelonGraph MCP server gives MCP clients read-only, keyless access to CVE intelligence and internet-exposure data from EchelonGraph.
Summarize and search the CVE feed (
cve_summary,search_cves) with severity, CVSS, EchelonGraph score, EPSS, CISA KEV, ransomware, and unassessed-score labels.Retrieve full CVE details (
get_cve), including KEV text, references, CPEs, aliases, and remediation summary.Explore internet exposure (
cve_exposure,exposure_radar): exposed ip:port services, KEV exposure totals, exposed databases, leaked credentials, shadow AI, and MCP-server counts.Track exploitation signals with
kev_recentfor recent CISA KEV additions andepss_historyfor recorded EPSS changes.Assess impact with
check_affectedfor a product/package version,check_sbomfor purls or CycloneDX/SPDX SBOMs, andscan_manifestfor lockfiles and pinned manifests.Investigate weaknesses and fixes with
cve_intel(CWEs, exploits/checks, affected packages, fixed branches),cve_remediation, andget_cwe.Search and read vendor advisories with
vendor_advisories_for_cve,get_vendor_advisory, andsearch_vendor_advisories.Use built-in prompts (
triage_cve,kev_weekly_brief,am_i_affected,sbom_review,workload_triage) and resources (echelongraph://methodology,echelongraph://sources,cve://{cve_id}), locally via npx or through the hosted HTTP endpoint.Every result carries structured context (
state,measured_at,method,coverage,freshness,notes, truncation metadata) so failures, zeros, and unassessed lookups are not mistaken for clean findings.
Provides tools to search and retrieve Cisco vendor advisories, including advisory details, severity, CVSS scores, affected products, and associated CVEs.
Provides exposure radar data for unauthenticated ClickHouse instances, including counts of exposed services and top countries/engines from EchelonGraph's internet-exposure scans.
Integrates with GitHub advisory data (GHSA) for vendor advisories and with public GitHub push events for leaked credential detection.
Provides tools to search and retrieve Palo Alto Networks vendor advisories, including advisory details, severity, CVSS scores, affected products, and associated CVEs.
Provides tools to search and retrieve Red Hat vendor advisories (e.g., RHSA), including advisory details, severity, CVSS scores, affected products, and associated CVEs.
Provides exposure radar data for unauthenticated Redis instances, including counts of exposed services and top countries/engines from EchelonGraph's internet-exposure scans.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@EchelonGraph MCP Servershow me the internet exposure footprint for CVE-2021-44228"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
EchelonGraph MCP server
CVE and internet-exposure data for Claude, Cursor, VS Code, 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).
Its 16 tools look up one CVE (record, exploit code and fixed versions, how it is fixed as its
sources state it, EPSS history, vendor advisories, exposure footprint), list CISA's newest KEV additions, check whether a product or
package version is affected, check an SBOM or a project's lockfiles, and read a CWE and its CVEs. Every result says how
it was measured (state, measured_at, method, coverage, freshness, notes), so a model
cannot mistake an outage or an unassessed lookup for an all-clear.
It also serves five prompts, ready-made workflows a client can show as slash commands
(triage_cve, kev_weekly_brief, am_i_affected, sbom_review, workload_triage; see "Prompts"), two resources
(echelongraph://methodology and echelongraph://sources) and one resource template
(cve://{cve_id}; see "Resources").
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.
npm:
echelongraph-mcp· Official MCP Registry:io.echelongraph/echelongraph-mcp· Docs: https://echelongraph.io/pulse/mcpSource: https://github.com/echelongraph/echelongraph-mcp · Changes: CHANGELOG.md · Security: SECURITY.md
Also listed on: Glama
Quick start
The local server runs through npx and needs Node.js 20 or later; nothing is installed
globally. Claude Desktop's one-click extension, below, needs neither. Add one of the blocks below
to your client, quit it fully and reopen it, then ask: "Is CVE-2023-44487 actively exploited, and
how many exposed services does EchelonGraph's radar have on record for it?"
Claude Desktop
Three ways in. The first two need no Node.js: a one-click extension that runs the server on your
machine, and a custom connector to the hosted endpoint. The third runs the server through npx.
1. Install in Claude Desktop
(one click, no Node.js). The link downloads echelongraph-mcp.mcpb, a Claude Desktop extension.
Open it, or in Claude Desktop choose Settings → Extensions → Advanced settings → Install Extension
and pick it, then choose Install. Claude Desktop runs the server with the Node.js it ships and
writes its own configuration: no PATH and no JSON to edit. To update, install the next release's
bundle the same way. Every new release on GitHub carries the bundle (releases up to 2.6.5 do not),
built from the release's tag by the workflow that publishes the npm package, and signed with a SLSA
build-provenance attestation. To check a download:
gh attestation verify echelongraph-mcp.mcpb --repo echelongraph/echelongraph-mcp.
2. Custom connector (no install). Claude Desktop uses your claude.ai account's connectors:
Customize → Connectors → + Add → Add custom connector. Name it EchelonGraph, paste
https://mcp.echelongraph.io/mcp, choose No sign in, and add it. A connector added on
claude.ai shows up in Claude Desktop too, and the plan rules are claude.ai's (see "Remote (no
install)" below).
3. Local server through npx.
Check Node.js. In Terminal (macOS) or PowerShell (Windows), run
node -v. It must printv20or later. If it prints an older version orcommand not found, install the LTS release from https://nodejs.org, or on macOS with Homebrew runbrew install node.Open the config file in a plain-text editor. Settings → Developer → Edit Config shows
claude_desktop_config.json(macOS:~/Library/Application Support/Claude/claude_desktop_config.json; Windows:%APPDATA%\Claude\claude_desktop_config.json). Open it in a plain-text editor such as VS Code, Notepad ornano. TextEdit on macOS turns"into curly quotes (“ ”) when Smart Quotes is on, and the JSON then breaks without an error: turn it off under Edit → Substitutions → Smart Quotes, or use another editor.Merge, do not replace. The file usually exists already and holds Claude Desktop's own settings (for example a
"preferences"object). Keep everything in it. Add an"mcpServers"key at the top level, next to the keys already there; if"mcpServers"is already there, add the"echelongraph"entry inside it. Mind the comma between entries. The result looks like this, with your own settings wherepreferencesis:{ "preferences": { "…": "the settings already in your file stay as they are" }, "mcpServers": { "echelongraph": { "command": "npx", "args": ["-y", "echelongraph-mcp"] } } }To check that the file parses, run this in Terminal (macOS):
node -e "JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'))" "$HOME/Library/Application Support/Claude/claude_desktop_config.json"or this in PowerShell (Windows):
node -e "JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'))" "$env:APPDATA\Claude\claude_desktop_config.json"No output means valid JSON. A
SyntaxErrormeans the file is broken (often curly quotes or a missing comma);ENOENTmeans there is no file at that path yet.Quit fully, then reopen. Closing the window leaves the old config loaded. On macOS choose Claude → Quit Claude (⌘Q); on Windows right-click the Claude icon in the system tray and choose Quit. The first start can take longer while
npxdownloads the package.Check that it worked. Settings → Developer lists
echelongraphas running, and the tools menu in the chat box lists EchelonGraph's tools, the ones under "Tools" below. Then ask the question at the top of this section.
Troubleshooting Claude Desktop
spawn npx ENOENT(or the server shows as failed). On macOS, an app opened from the Dock does not get your shell's PATH, so Claude Desktop cannot findnpxwhere Homebrew (Apple silicon:/opt/homebrew/bin; Intel:/usr/local/bin) or nvm put it. Runwhich npxin Terminal, then give Claude Desktop that absolute path as"command", and the folder it is in asPATHunder"env"(npxstartsnodefrom that same folder). With Homebrew on Apple silicon:{ "mcpServers": { "echelongraph": { "command": "/opt/homebrew/bin/npx", "args": ["-y", "echelongraph-mcp"], "env": { "PATH": "/opt/homebrew/bin:/usr/bin:/bin" } } } }With nvm, use the full path
which npxprints (JSON does not expand~). Then quit fully and reopen. On Windows, install Node.js with the installer from nodejs.org, which addsnpxto the system PATH, then quit Claude Desktop fully and reopen it.The server does not appear at all. The file is not valid JSON (a missing comma, or curly quotes), or the app was not fully quit: see steps 2 to 4.
Logs. macOS:
~/Library/Logs/Claude/mcp-server-echelongraph.log(this server's output) and~/Library/Logs/Claude/mcp.log(connections). Windows:%APPDATA%\Claude\logs\, with the same file names.
Claude Code
claude mcp add --transport stdio echelongraph -- npx -y echelongraph-mcpAdd --scope user before the -- to make it available in every project, or --scope project
to share it through the project's .mcp.json. Claude Code hands the model each result's
structuredContent, which this server holds to 30,000 characters unless a call passes full: true
(see "Long answers"); MAX_MCP_OUTPUT_TOKENS (25,000 tokens by default) is the cap Claude Code
applies to a tool's output, and raising it lets a full: true answer through.
Cursor
~/.cursor/mcp.json (every project) or .cursor/mcp.json (one project):
{
"mcpServers": {
"echelongraph": {
"command": "npx",
"args": ["-y", "echelongraph-mcp"]
}
}
}VS Code
.vscode/mcp.json in the workspace (VS Code's key is servers, not mcpServers):
{
"servers": {
"echelongraph": {
"command": "npx",
"args": ["-y", "echelongraph-mcp"]
}
}
}or from a terminal:
code --add-mcp '{"name":"echelongraph","command":"npx","args":["-y","echelongraph-mcp"]}'Windsurf, Cline and other clients
Clients that read an mcpServers block (Windsurf's mcp_config.json, Cline's MCP settings,
and most others) take the same block as Cursor above.
Remote (no install)
The same 16 tools, five prompts, two resources and one resource template are served over Streamable HTTP at:
https://mcp.echelongraph.io/mcpKeyless, no sign-in, stateless; both protocol eras. The hosted endpoint is in service:
https://mcp.echelongraph.io/health answers {"status":"ok","version":…} with the package
version it runs.
claude.ai (Free, Pro, Max, Team and Enterprise plans): Customize → Connectors → + Add → Add custom connector. Name it
EchelonGraph, paste the URL, choose No sign in, and add it. On Team and Enterprise an Owner adds it for the organization first, and each member then connects it. Free plans allow one custom connector.Claude Code:
claude mcp add --transport http echelongraph https://mcp.echelongraph.io/mcpCursor (
mcp.json):{ "mcpServers": { "echelongraph": { "url": "https://mcp.echelongraph.io/mcp" } } }VS Code (
.vscode/mcp.json):{ "servers": { "echelongraph": { "type": "http", "url": "https://mcp.echelongraph.io/mcp" } } }Windsurf (
mcp_config.json):{ "mcpServers": { "echelongraph": { "serverUrl": "https://mcp.echelongraph.io/mcp" } } }
The hosted endpoint runs this package's code (dist/http.js, below) against the same public
API, so its answers are the npm package's answers ("Privacy" says what it logs).
Related MCP server: CVE-Search MCP Server
Tools
Every tool is read-only (readOnlyHint: true) and declares a title and an outputSchema.
The example questions are ones a client can answer with that tool alone.
Tool | Title | What it answers | Example question |
| CVE feed summary | Counts of active CVEs by severity band, the count with no severity band from any source ( | How many critical CVEs does the feed hold, and when was it last updated? |
| Search CVEs | Search/filter CVEs (severity, min CVSS, text, sort) with EchelonGraph scores and | Find critical CVEs that mention Tomcat with a CVSS of 9 or more. |
| CVE detail | One CVE's record: CVSS v3 and (when scored) v4, the EchelonGraph score and its confidence, whether EchelonGraph has scored it ( | What are CVE-2024-3094's CVSS, EPSS and CISA-KEV status? |
| Internet exposure for one CVE | Internet-exposure footprint for a CVE: exposed service count (distinct ip:port, the | How many exposed services does EchelonGraph's radar have on record for CVE-2023-44487? |
| Exposure radar totals | 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; and MCP servers found in EchelonGraph's own Certificate Transparency feed, by RFC 9728 verdict, protocol era and transport. Every number is labelled by what it counts, and a field the tool cannot label is left out and named. | How many unauthenticated data stores has EchelonGraph's radar confirmed? |
| Recent CISA KEV additions | The CVEs CISA has added to its Known Exploited Vulnerabilities catalog, newest first ( | Which CVEs has CISA added to KEV since 1 September, and which have known ransomware use? |
| EPSS change history for one CVE | How one CVE's EPSS score has changed, as EchelonGraph recorded it: one point per recorded change ( | How has CVE-2023-44487's EPSS score changed, as EchelonGraph recorded it? |
| Am I affected? (product or package at a version) | Whether a product (its NVD CPE product token) or a registry package ( | Is openssl 3.0.0 affected by known CVEs? Is lodash 4.17.15 on npm? |
| Check purls or an SBOM against the advisory corpus | A dependency list checked against EchelonGraph's advisory corpus (OSV.dev records), one verdict per component: | Check this CycloneDX SBOM against the advisory corpus. |
| Check a lockfile against the advisory corpus | A project's lockfiles or pinned manifests checked against the same advisory corpus, one verdict per dependency, through | Check this package-lock.json against the advisory corpus. |
| CVE weakness, exploits and packages | Weakness, exploit code and detection checks (a Nuclei template, or a GitHub repository the API serves as a checker, checks whether a system is affected; it is not necessarily exploit code, and the note counts them apart), affected packages and fixed versions for one CVE, from EchelonGraph's per-CVE enrichment: | Is there public exploit code for CVE-2021-44228, and which versions fix it? |
| How one CVE is fixed | How one CVE is fixed, as its sources state it, in one answer; every text is relayed as its source states it, untested by EchelonGraph: | How is CVE-2021-44228 fixed, as its sources state it? |
| CWE and its CVEs | One CWE (weakness class) and the CVEs classified under it: | Which CVEs are classified under CWE-79, CISA-KEV-listed first? |
| Vendor advisories for one CVE | The vendor-published advisories (Microsoft MSRC, Red Hat, Cisco, Palo Alto Networks, GitHub GHSA and the other feeds EchelonGraph polls) that name one CVE, newest first, at most 20, each with | Which vendor advisories name CVE-2024-3400? |
| Vendor advisory detail | One vendor advisory in full: description, severity, | Show one of those advisories in full: affected products, remediation and references. |
| Search vendor advisories | Search vendor advisories by text (title, description, vendor, advisory ID, products, CVE IDs), vendor, severity and whether they name a CVE. The search text is sent in a request header, never in the URL. | Search vendor advisories for FortiOS, critical severity only. |
The three vendor-advisory tools relay vendor_published_at (the vendor's date), our_first_seen_at
(when EchelonGraph first recorded the advisory) and withdrawn (the vendor rescinded it; the note
names each one, and the search leaves them out).
Each also relays every vendor's window in coverage (vendor_windows, or vendor_window for the
detail): the advisories held, the earliest and latest vendor_published_at among them,
held_since, feed_held_since, history_backfill and history_scope. An advisory published
before its vendor's held_since may not be held, nor, where a vendor's history read covers only
part of its advisories (history_scope reviewed: GitHub's), one outside that part published
before its feed_held_since. vendor_advisories_for_cve lists in vendors_not_fully_held each
vendor absent from its answer that may not hold every advisory for the CVE ID's year: no advisory
from such a vendor is not a finding that it published none
(how each window is read).
Every figure is as fresh as the last check that produced it. The CVE feed is polled from its
sources on a schedule. Each radar has its own schedule and refreshes only while it runs, and
exposure_radar's freshness gives each radar's last completed check (last_run_at) where the
API serves one.
How each tool reads its answer
What each tool relays, field by field, and what each number counts, is in the pages below, which
ship in the package's docs/ folder:
How
cve_summaryreads its counts:summary.nonecounts CVEs not yet scored, not CVEs rated None, andsummary.nvd_criticaltosummary.nvd_nonecount by NVD's severity label (or a pre-NVD label standing in), as provenance.How
get_cveandsearch_cvesread the EchelonGraph score:score_assessed: falseis not yet scored, never a score of 0.What
patch_availablesays, and the evidencepatch_evidencenames, the CVE record's own fix statement among it (cna_solution,description_fix):falsemeans no fix evidence is on record, not that no fix exists.How
cve_exposurecounts: a count of ip:port services, not machines, inferred from banner versions in a sample of Shodan data, and for a CVE outside the radar's tracked set NOT ASSESSED: its 0 is not a measurement.How
exposure_radarcounts: every number of its five radars, each labelled by what it counts.How
check_affecteddecides: readassessedbeforecount; a registry match'sfixed_inis the lowest version above it that no affected interval of the record holds, or null withfixed_in_reason.
Prompts
Ready-made workflows a client can show as slash commands. Each prompt names the tools to call, in
order, and tells the model to read every result's state, measured_at, method, coverage
and notes: not_assessed is never reported as clean, a count of 0 under assessed: false is
never "not affected", and each figure cites its measured_at. A prompt makes no API request.
Prompt | Arguments | What it asks the model to do |
|
|
|
|
| Work out the start date from |
|
|
|
|
| Pass the document to |
|
| Rank your own pods, CI job containers or images: list the workloads and decide their exposure with your own tools, make an SBOM of each image digest, and pass |
Resources
Two resources, which resources/list lists, and one resource template, which
resources/templates/list lists (a client reads cve://CVE-2024-3400, for example):
Resource | What it holds |
| What each envelope field and |
| What the API reports about its feeds when the resource is read: the NVD poller's |
| One CVE as |
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 (cut to fit 30,000 characters when it is longer: see "Long answers" below); 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 exceptions to verbatim are
cve_summary, which leaves out apollerfield the answer sends in a JSON type other than its outputSchema's (or apollerthat is neither a JSON object nor null) and names it in the note, andexposure_radar: each radar is cut to the fields docs/exposure-radar.md lists, and eachkev_exposure.newest_kevrow gains anexposure_state. Itsshadow_aicounts are regrouped as that page says, 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, so a
client that passes only content to the model still sees how the answer was measured.
Long answers
A model pays for every character it reads, and clients differ in which part of a result they hand it, so each part is held to the same budget, 30,000 characters, and each is cut on its own terms:
Claude Code hands the model
structuredContent, not the text blocks, and sizes it againstMAX_MCP_OUTPUT_TOKENS(25,000 tokens by default): it warns past 10,000 tokens and, past the cap, saves the result to a file and hands the model its path instead. Through 2.7.xstructuredContent.datawas always the answer whole (get_cveCVE-2021-44228: 65,540 characters on 2026-10-11, which Claude Code saved to a file). Since 3.0.0 it is cut to fit (below).MAX_MCP_OUTPUT_TOKENS, set in the environment Claude Code starts in, raises its cap.A client that passes only the text blocks (
content) reads the first text block, cut to fit as described after the next item.A program that reads
structuredContent.dataand needs the answer whole, however long, passesfull: true: every tool takes it. A client that sizes structured results, as Claude Code does, can refuse such an answer past its cap.
The structured result. When the whole of it (the envelope, its notes and data, as compact JSON)
would pass 30,000 characters and the call does not say full: true, data is cut, in an order each
tool states. Where one row is large, every row is first thinned, all rows kept: check_sbom and
scan_manifest keep each row's verdict and cve_ids, and every match field a prompt reads, and
their not_affected and not_assessed rows leave first where that lets every affected row keep
all its matches (how);
check_affected keeps each
match's scores, flags, criteria and fixed_in, then fewer; cve_remediation drops each item's
product_ids, then its text, keeping every advisory's state, kinds, URLs and fixed builds
(get_vendor_advisory returns each text), thins fixed_branches (then caps each
package's ranges) and patches, then cuts fix_references, then patches, then
fixed_branches, advisories last, the note naming vendor_advisories_for_cve, which
lists them all; search_cves keeps each row's
description and scores, then the scores alone; the vendor-advisory
tools drop affected_products, then summary. Only then do rows leave (check_sbom and
scan_manifest: as that page says); check_affected drops its excluded and undetermined samples first and its
cve_ids, which names every match, last; the other row tools drop rows from the end, and those that
page say the offset or limit to read them from (search_cves and search_vendor_advisories
also the page size whose rows fit data whole, counted on the rows whole). get_cve cuts
cpe_match and references, each to at most the same number of entries, then
cpe_configurations; exposure_radar cuts its daily and weekly series, each keeping its newest
entries, then every ranking, then kev_exposure.newest_kev; then any other list at the top of
data is cut the same way. The room left once a list fits goes back to the lists emptied before
it, as many of their entries as fit (their first, a series its newest). No string is shortened and no value changes (a thinned row
keeps some of its fields, each optional in the outputSchema, so data still validates;
a cut adds only matches_truncated and fixed_branches_truncated). The result
then says data_truncated: true (false on every other success), truncated gives each list cut,
by its path in data, with its total, the number returned and, where its entries are thinned,
thinned: true, and the note ends with sentences starting DATA CUT that say what was cut, how to
read the rest and that the same call with full: true returns data whole.
The first text block. Through 2.6.2 it was the API's JSON whatever its size. So
when the API's JSON is longer than 30,000 characters, the first text block holds it
cut to fit, cut from the answer whole (not from the cut data), the same way with or without
full: true. First the same JSON is laid out with
each row on a line of its own, which drops no character of the answer but the indentation. If
that is still too long, a tool that returns a list of rows cuts each row to the fields it names,
most kept first (search_cves keeps cve_id, severity, cvss_v3_score, echelongraph_score,
score_assessed, epss_score, kev_listed and the first 200 characters of the description,
among others), with every long string ending in "…". A list beside the rows is cut before any
row leaves the text: check_affected's
excluded and undetermined samples keep their first 10 entries, each its cve_id and
reason, and its cve_ids (each match's cve_id, in order) its first 10, so that every CPE
match stays in the text, each with at least cve_id, kev_listed, ransomware, epss_score,
effective_score and score_assessed (and fixed_in on a registry match; a registry list near
the cap of 200 can leave its last matches out, KEV-listed and ransomware ones last, and the note
says how many and names any of those), and check_sbom's
not_sent_purls keeps its first 10, the note saying from which position of the input the purls
not sent run, and in what order the input's purls are counted, so that a second call can send
them without reading the list. scan_manifest cuts its rows as check_sbom does, and its
not_checked list keeps its first 20 entries. cve_remediation thins its advisories, then
fixed_branches and patches, then caps each package's ranges at the most that fit (a package so
cut carries fixed_branches_truncated and is named in the note, which says to read its fix with full: true), then drops fix_references,
patches and fixed_branches rows, in data's order. When even the leanest cut is too long, rows are left out of the text
(check_sbom and scan_manifest leave out the not_affected rows first, then the not_assessed ones, and rows with a KEV-listed or ransomware match last, naming by index any that leave; those two kinds are cut to the leanest level and leave sooner where that lets the other rows keep their matches, and where it does not, every row is cut to that level first: see docs/tools.md), and the
note says how to read them: the offset to call next, a smaller limit, or, for advisories,
vendor_advisories_for_cve. Any other answer
keeps the first entries of each of its lists (get_cve's cpe_match and references). The note
then ends with one sentence starting TEXT CUT that says what the first text block leaves out
and which tool returns a row (get_cve, get_vendor_advisory, check_affected, and
cve_intel a CVE's fixed versions): whole in that tool's structuredContent.data with
full: true, since its own first text block is cut past 30,000 characters too. An answer that
fits is sent as before, pretty-printed and whole. The same answer is always cut the same way. The
cve://{cve_id} resource reads get_cve's record whole and cuts its data as get_cve's first
text block is cut, and says so: data_truncated: true, truncated naming each list it cut, and a
note starting DATA CUT. The prompts ask only for what the
text keeps: kev_weekly_brief reads kev_recent 50 rows a page, which the text holds whole;
triage_cve reads CISA's due date from get_cve; and sbom_review reads each match's fixed_in,
and cve_intel's fixed_branches where the text keeps no match for a component.
Structured results
Since 2.0.0 every result a tool answers, 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 a client treats as untrusted).
Field | What it holds |
|
|
| When the underlying observation was made, as the API states it. |
| How the numbers were produced. |
| What the answer covers, where the tool can say: |
| The producing radar's last completed check ( |
| The caveats, one sentence each: what the envelope itself needs saying, then the note from the text block. |
| On a success only: |
| When |
| On a success only: the API's JSON, whole with |
| On a failure only: |
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 (on a success
data_truncated and truncated, 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 structured result, but
for what a TEXT CUT leaves out (see "Long answers"), and cannot disagree with it;
beside a DATA CUT, the first text block is cut from the whole answer, not the cut data.
| What it means |
| A measurement of what was asked. An exposure count is |
| 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: |
| The lookup did not complete. Not a finding. |
| The input was refused, by this server or by the API, so nothing was looked up. Not a finding. |
What each tool's state, measured_at, coverage and freshness hold, tool by tool, is in
docs/structured-results.md.
Every field comes from what the API sends; where the API does not say, the field is null and a
note says so.
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, with one
exception: a cve_summary poller field of another JSON type is left out of data and named in
the note, and the rest of the answer is relayed, summary with it. A
field the API adds later is still relayed by every tool that relays the API's JSON as data, and
exposure_radar and cve_intel, which relay a selection, leave it out and name it.
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, prompts and resources capabilities 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, which it keeps. 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; that Shodan data is
Shodan's; what state, measured_at and freshness mean, and that each result carries them in
its structured result and again in its text; and that exposure numbers are aggregate counts of
ip:port services, not an internet-wide census.
The instructions and every tool description are at most 2,048 characters, the shortest cut a
client is known to make, so no client that cuts there loses a sentence. A description says what its tool
returns and the rules that keep a zero, a false or a failure from reading as a finding; what each
field counts, in full, is in the tool's outputSchema, and the result's note says it again for the
numbers it relays.
Privacy: what is sent where
The npm package (stdio). The server runs on your machine and sends HTTPS requests to one
host, ECHELONGRAPH_API_BASE (default https://app.echelongraph.io), one per API call a tool
makes (exposure_radar makes five; every other tool makes one). No telemetry: it sends nothing
else, to EchelonGraph or anyone. Every request carries the User-Agent
echelongraph-mcp/<version> (+https://echelongraph.io/pulse/mcp), with the
ECHELONGRAPH_MCP_UA token ahead of it when you set one, so the API's access log can count use
per version. Like any HTTPS request, it also reaches the API from your network address.
Where each tool's input travels:
Input | Where it is sent |
A CVE ID ( | The URL path. |
| The |
| The |
| The |
| The |
| The |
| The purls, in POST bodies of at most 200 each. A CycloneDX or SPDX document is read on your machine and only the purls in it are sent; the document is not. (Over the hosted endpoint the document goes to EchelonGraph's server instead: see below.) |
| The purls read from the files, in POST bodies of at most 200 each; the filenames, the files' text and the entries not checked are not sent on to the API. Run from npm, the server that reads them is on your machine; over the hosted endpoint it is EchelonGraph's, and the files are the request body (see below). |
| No input. |
The API marks its answers to header-carried lookups no-store, so no shared cache keeps them,
and request logs and trace spans that record URLs do not hold what was looked up there.
The hosted endpoint. A request to https://mcp.echelongraph.io/mcp reaches EchelonGraph's
server first, which then calls the same API as above. Per request it logs the HTTP method (GET,
HEAD, POST, PUT, PATCH, DELETE, OPTIONS, CONNECT or TRACE, else (other)) and path, the status,
the duration, its wait for a place (and for a large-body slot), the requests served at once, its
large-body slots held (and the oldest hold's age), the body size, the JSON-RPC method (one MCP
defines, else (other)) and the tool or prompt name, the protocol version, whether an Origin
header was sent, the first word of your client's User-Agent, whether your address is public (not
the address), whether your client closed early, and whether it was throttled or refused, and why.
When one of its API calls fails, it logs the tool and JSON-RPC method, the call's route without
the ID or query (/api/v1/public/cves/:id), what failed (such as a timeout, a reset or the status
the API answered), how long it took, and whether the request was our monitor's. An error it logs
by its type, a fixed cause label and its code, with its request's JSON-RPC method and protocol
version, never by its message. It never logs tool arguments or a query string. A check_sbom document (or
an sbom_review prompt's) is a tool argument like any other here: it is in the request body, so
the whole document reaches EchelonGraph's server, which reads the purls from it in memory, sends
only those to the API, and neither logs nor keeps the document. To keep the document on your
machine, run the npm package, or pass check_sbom the purls. scan_manifest's files are the
same: their whole text is in the request body, read into purls in memory, and only those go to
the API; the files are neither logged nor kept. To keep lockfiles on your machine, run the npm package. Past
the per-client limit it logs that client's address key. It passes your client's public address to
the API, in a header the API trusts only with a token it checks, so the API's rate limit counts
you and not the hosted service. The API's access line then records each of those calls under
your address (an IPv6 address by its /64) with its URL path: for a CVE, CWE or advisory lookup,
the ID you asked about. It does not record the query string or the header-carried terms above
(for search_cves, whether a term was given and its length, not the term), and of check_sbom's
and scan_manifest's purls it records how many, not which.
EchelonGraph's privacy policy: https://echelongraph.io/privacy.
Rate limits
No API key, so the API limits each caller, one IPv4 address or one IPv6 /64 network, counting
all its requests together in a 60-second window: 12,000 a minute to /api/v1/public/cves and the
paths under it, 6,000 to the CWE, vendor-advisory, KEV and shadow-AI paths, and 600 to any other
public path; check_sbom's and scan_manifest's batch route is charged per component, 1,200
components a minute. Over a limit the tool returns a failure (state failed, error.status
429), never empty data; check_sbom and scan_manifest wait out a 429 (their rows). The
hosted endpoint adds 120 MCP requests a minute per client address, refuses a request body over 64
KiB (6 MiB for a check_sbom or scan_manifest call, or an sbom_review prompt), and one
whose reads would take it past 30 MiB (about a 3 MB poetry.lock).
Every limit, path by path:
docs/rate-limits.md.
Configuration
The npm package (stdio):
Env var | Default | Purpose |
|
| Override the API base (self-host / proxy). A credential in the URL is masked wherever a result quotes the base. |
|
| Per-request timeout. A slower answer is reported as a failed lookup, not as empty data. |
| unset | One product token (for example |
The HTTP entrypoint, dist/http.js (npm run start:http), serves the same tools over
Streamable HTTP on POST /mcp, with a liveness check on GET /health. In a small container, cap
V8's heap (docs/rate-limits.md says how). It reads the three variables above and:
Env var | Default | Purpose |
|
| The port it listens on. |
|
| MCP requests a minute per client address, per instance. |
|
| The largest request body it reads, except for an SBOM (below). |
|
| The largest body of a |
|
| How many bodies over |
|
| How many requests it serves at once, not counting |
|
| How long a request waits to be served before it is answered 503 with |
|
| A body that sends nothing that long while it is read is answered 408; a request running that long in all is stopped (docs/rate-limits.md). |
| unset |
|
| unset | EchelonGraph's hosted deployment only: the token with which it names each client to the API. Leave them unset when you run it yourself; the API then counts every request as coming from your server. |
Develop
npm install
npm run build # tsc → dist/
npm test # build, then the behavioural suite over both protocol eras, against a stub API
npm run smoke # spawn the server + call cve_exposure against the production APInpm run smoke calls the production API with this package's User-Agent: from any machine but
our workspace, it counts as MCP adoption unless you set ECHELONGRAPH_MCP_UA (see Configuration).
npm test never leaves the machine.
npm test runs over a 2026-07-28 server/discover and a 2025-06-18 initialize. To test an
installed package instead of dist/, set ECHELONGRAPH_MCP_BIN to its echelongraph-mcp bin.
Versioning
Semantic versioning: a major version changes protocol behaviour or drops a Node.js version, a
minor version adds tools or fields, a patch fixes wording or behaviour without changing a tool's
shape. Every release is published to npm with a provenance attestation by the public repo's
release workflow, from the commit tagged v<version>. Changes per version:
CHANGELOG.md.
Since 3.1.0, a success's outputSchema is open at every level: each object admits fields it does
not name, and each enum but state any string beside the values it lists; a coverage or
freshness a tool answers null admits an object too, so a field or value added in a minor
release does not fail a client holding a 3.1.0 or later tool list. The failure branch keeps
every object closed, and every enum but four, which already accept any string:
coverage.not_assessed_reason (check_affected), coverage.vendor_windows[].history_backfill
(vendor_advisories_for_cve, search_vendor_advisories) and coverage.vendor_window.history_backfill
(get_vendor_advisory). A client that listed the tools under 3.0.2 or older must reconnect once
(the CHANGELOG names the calls). A client showing "does not match the tool's output schema" holds
an older schema: reconnect it (Claude Code: /mcp, then reconnect echelongraph) or restart it.
Security
Report a vulnerability privately to support@echelongraph.io, not in a public issue. The policy, the supported versions and the scope are in SECURITY.md.
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.
Available Tools
16 toolscheck_affectedAm I affected? (product or package at a version)ARead-onlyIdempotentInspect
Whether a product or package at a given version is affected by known CVEs, from the same matcher as echelongraph.io/am-i-affected. The CPE path takes product (an NVD CPE product token, such as nginx) and version, and returns the CVEs whose NVD CPE match criteria include that product at that version; each match names the vendor NVD asserts (cpe_vendor) and whether it was verified (vendor_unknown). The registry path takes ecosystem (npm, PyPI, Maven and other OSV ecosystem names), package and version, and decides each OSV advisory EchelonGraph holds for that package as affected, not affected or undetermined. count depends on assessed: assessed false means the lookup did not evaluate this component, not_assessed_reason says why, and a count of 0 there is not a finding of not affected. An advisory undecidable at this version is reported as undetermined (undetermined_count, and up to 50 of them in undetermined), never as safe. Each match carries cve_id, kev_listed, ransomware, epss_score, effective_score, effective_severity and score_assessed (false: not yet scored, so echelongraph_score is withheld). A registry match also carries interval, the advisory interval holding it, and fixed_in, the lowest unaffected version above it, or null with fixed_in_reason; a CPE match carries no fixed_in. All four inputs travel in request headers, never in the URL. Past 30,000 characters of JSON, the first text block holds data cut to fit, and the note says what the cut leaves out and where to read it (TEXT CUT); so is structured data (DATA CUT), unless full: true. Cut, the excluded and undetermined samples keep their first 10 entries, each its cve_id and reason, cve_ids keeps its first 10, and each match keeps cve_id, kev_listed, ransomware, epss_score, effective_score, score_assessed and fixed_in at least. Every CPE match stays in the text; near the cap, a registry list's last matches can leave it, and the note says how many.
Inputs: version (required), full, package, product, +1 more. Returns: object
check_sbomCheck an SBOM against the advisory corpusARead-onlyIdempotentInspect
Check a dependency list against EchelonGraph's advisory corpus, one verdict per component. For container images and Kubernetes pods, the input is an SBOM of each image, or its purls. Pass purls (package URLs, up to 2,000 distinct, 10,000 with repeats) or sbom (a CycloneDX or SPDX JSON document, up to 5,000,000 characters). This server reads the document's purls; only they are sent to the API, in POST bodies of at most 200, never in a URL; the document itself is not sent on. Run from npm, this server is on your machine; over the hosted endpoint (mcp.echelongraph.io) it is EchelonGraph's, and the document is the request body, accepted up to 6 MiB. data.results holds one row per component sent, in order, with verdict (affected, not_affected, undetermined or not_assessed), not_assessed_reason, cve_ids, matched_package and matched_via (a deb or apk purl's upstream qualifier matches as its source package); each match carries fixed_in (the next unaffected version) or null with fixed_in_reason. data.summary counts the verdicts. not_affected is the only clean verdict: not_assessed (no verdict, as for a deb, apk or rpm purl without a distro qualifier naming its release, which EchelonGraph does not guess) and undetermined are not, and the note counts both. The API allows 1,200 components a minute per caller; on a 429 the tool waits out Retry-After, up to 50 seconds a call, then answers what it has, the rest in data.not_sent_purls, not clean. Input with more than 2,000 distinct purls is refused, not truncated. Past 30,000 characters of JSON, the first text block holds data cut to fit, and the note says what the cut leaves out and where to read it (TEXT CUT); so is structured data (DATA CUT), unless full: true. Cut, each row keeps index, purl, verdict, not_assessed_reason and cve_ids at least, each match its fixed_in while kept; not_affected rows leave the text first, then not_assessed ones; not_sent_purls keeps its first 10.
Inputs: full, sbom, purls. Returns: object
cve_exposureInternet exposure for one CVEARead-onlyIdempotentInspect
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 data is owned by Shodan, which holds its copyright (© Shodan). 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. 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. last_seen is when EchelonGraph last wrote or refreshed a service's row, not when the service was observed: a port still listed by Shodan InternetDB refreshes it 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. Its structured result's state is not_assessed with measured_at null, since the answer dates no observation; 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; data is the API's JSON. Past 30,000 characters of JSON, the first text block holds data cut to fit, and the note says what the cut leaves out and where to read it (TEXT CUT); so is structured data (DATA CUT), unless full: true.
Inputs: cve_id (required), full. Returns: object
cve_intelCVE weakness, exploits and packagesBRead-onlyIdempotentInspect
Weakness, exploit code, detection checks, affected packages and fixed versions for one CVE, from EchelonGraph's per-CVE enrichment. Returns cwes (each cwe_id with name and source), exploits (each with kind, source_name, source_url, source_published_at (the source's date, null if none), eg_first_seen_at (when EchelonGraph recorded it) and verified_status; at most 10, verified first) with exploits_total, exploits_capped, exploits_by_kind and exploits_by_status, affected_packages (ecosystem, package_name, version_range, fixed_version, fixed_branches), fixed_versions (each fixed version with the vulnerable_range it fixes) and timeline (newest enrichment-history rows, with timeline_total). fixed_branches lists each affected range on record (from introduced ("0" is the first version) to below fixed, or through last_affected, with advisory_id (its OSV record) and source); in the ecosystem's version order, an affected version's fix is the first fixed above it that no range of its row holds (none past a range with no fixed). fixed_version is one range's fix, kept for compatibility, not the fix for every affected range. fixed_branches [] is not a finding that no fix exists. verified_status is the source's label, not a guarantee the exploit works on a given system. An empty exploits list is not evidence that no public exploit exists: it covers only the sources EchelonGraph ingests, and which are polled depends on the deployment. A section the API could not read is named in coverage.sections_failed and left out of data, never relayed as an empty list. Its structured result carries notes, with data, which the first text block holds whole up to 30,000 characters; its coverage names the sections relayed, failed and left out. Past 30,000 characters of JSON, the first text block holds data cut to fit, and the note says what the cut leaves out and where to read it (TEXT CUT); so is structured data (DATA CUT), unless full: true.
Inputs: cve_id (required), full. Returns: object
cve_remediationHow one CVE is fixedARead-onlyIdempotentInspect
How one CVE is fixed, as its sources state it, in one answer. Every text is relayed as stated by its source; EchelonGraph has not tested it. An empty list or a remediation_state of none_in_source or not_parsed is not a finding that no fix exists. Returns fix_status (the API's fix-status rule, as CVE Pulse shows it: state and basis), cisa (for a KEV-listed CVE CISA's required_action, due_date, short_description and notes_urls, verbatim; CISA's required_action is directed at US federal civilian agencies (under BOD 26-04, which replaced BOD 22-01 on 10 June 2026) and is not EchelonGraph's advice.), fixed_branches (each affected package with fixed_branches: every affected range, introduced and fixed or last_affected, and fixed_version, one range's fix), vendor_remediations (each vendor advisory naming the CVE, newest first, at most 20, with remediation_state, remediation_kinds and items, each with kind, source_category, text and url), fix_references (references NVD or the CNA tagged Patch or Mitigation), patches, coverage and failed_sections. kind is the vendor's own category, never read from the text: vendor_fix, workaround, mitigation, no_fix_planned, none_available or other. remediation_state is parsed, none_in_source (none listed for this CVE), not_parsed (that vendor's remediation is not read) or withdrawn. ecosystem, package and version (all three or none, sent in request headers, never in the URL) add installed, the verdict at that version, and fixed_in, the lowest unaffected version above it, or null with fixed_in_reason. A section the API could not read is named in failed_sections, not relayed as empty. A REJECTED CVE answers not_assessed. Past 30,000 characters of JSON, the first text block holds data cut to fit, and the note says what the cut leaves out and where to read it (TEXT CUT); so is structured data (DATA CUT), unless full: true. Cut, vendor texts are shortened first; kinds, states and URLs are kept.
Inputs: cve_id (required), full, package, version, +1 more. Returns: object
cve_summaryCVE feed summaryBRead-onlyIdempotentInspect
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 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, and names those CVEs not yet scored, not CVEs rated None. summary.nvd_critical, summary.nvd_high, summary.nvd_medium, summary.nvd_low and summary.nvd_none count the same active CVEs as summary.total by NVD's CVSS severity label: provenance, never EchelonGraph's severity band. summary.nvd_none 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, which summary.total and the other counts above leave out: they are withdrawn records, none of them a vulnerability. poller holds the in-memory counters of the NVD poller of the one API instance that answered, counted since that instance last started and zeroed on every restart: they describe that instance, never the feed's size, intake, reliability or freshness. Whenever the answer carries poller the note says so. Its structured result's state is measured, with freshness.nvd_poll the fleet's last completed NVD poll and its interval, from the answer's nvd_poll (freshness null without one), and data equal to the API's JSON less each poller field (or the whole poller) the note names as left out. Past 30,000 characters of JSON, the first text block holds data cut to fit, and the note says what the cut leaves out and where to read it (TEXT CUT); so is structured data (DATA CUT), unless full: true.
Inputs: full. Returns: object
epss_historyEPSS change history for one CVEARead-onlyIdempotentInspect
How one CVE's EPSS score (FIRST's exploit-prediction probability, 0 to 1, with its percentile) has changed, as EchelonGraph recorded it: points, oldest first, each with at, epss_score, epss_percentile and score_date (FIRST's score date, null where the record does not hold it); current, the record's value now (epss_score, epss_percentile, epss_updated_at); series_kind, always change_only; series_starts_at, when EchelonGraph began recording EPSS changes for any CVE; complete_since, from when the record holds every change; coverage, which FIRST score dates the record holds; history_rows, points_truncated and latest_point_matches_current. Pass a CVE ID like CVE-2023-44487. The series is change-only: a point is a recorded change, and a day without a point is not a recorded value. A daily series interpolated from it holds values EchelonGraph never recorded, so the series is never a daily series. Before series_starts_at nothing was recorded, so a missing point there means not recorded, not unchanged, and the value in force before a CVE's first point is not in the series. Between series_starts_at and complete_since the record misses changes, so a missing point there does not mean unchanged either; only from complete_since on is every change a point. coverage.days_missing and coverage.days_partial name the FIRST score dates the record does not hold, or holds for some CVEs only. latest_point_matches_current false means a change is missing from the series. Its structured result's measured_at is current.epss_updated_at (when EchelonGraph last wrote a changed value, not FIRST's score date), its coverage says which part of the record is complete and which score dates it holds, and its freshness is null. Past 30,000 characters of JSON, the first text block holds data cut to fit, and the note says what the cut leaves out and where to read it (TEXT CUT); so is structured data (DATA CUT), unless full: true.
Inputs: cve_id (required), full. Returns: object
exposure_radarExposure radar totalsARead-onlyIdempotentInspect
Aggregate totals from EchelonGraph's internet-exposure radars: kev_exposure, internet-facing services running actively-exploited (CISA-KEV) CVEs, plus the ransomware-linked subset, derived from Shodan data; exposed_databases, 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 (not a pure read: on Redis it names its client, and on ClickHouse its query is recorded in the server's query log); leaked_credentials, sampled from public GitHub push events; and shadow_ai, services found through Certificate Transparency logs and Shodan and then checked by EchelonGraph's identified probes. Shodan data is owned by Shodan, which holds its copyright (© Shodan). It also gives MCP-server counts: hostnames named like an MCP server in EchelonGraph's own Certificate Transparency feed (no Shodan data), each counted once by its latest verdict from EchelonGraph's identified MCP probe, which never sends tools/call. Service counts are distinct ip:port services, not machines. The result's note, and the outputSchema's description of each radar, label every number by what it counts. Of the shadow_ai numbers, only confirmed_exposed counts exposed services. A kev_exposure.newest_kev CVE whose exposure_state is not_assessed has no measurement in the answer: its 0 is not one. Its structured result's state is not_assessed with measured_at null, since no radar dates what it counts; each count is still relayed as what that radar holds on record. freshness gives each radar's last_run_at where the API serves one; coverage names the radars that answered. Past 30,000 characters of JSON, the first text block holds data cut to fit, and the note says what the cut leaves out and where to read it (TEXT CUT); so is structured data (DATA CUT), unless full: true.
Inputs: full. Returns: object
get_cveCVE detailARead-onlyIdempotentInspect
One CVE's record: description, severity, cvss_v3_score, cvss_v4_score and cvss_v4_severity, echelongraph_score, echelongraph_severity and score_confidence, epss_score and epss_percentile, CISA-KEV status (kev_listed, kev_added_date, kev_due_date, kev_ransomware) and, if KEV-listed, CISA's own text relayed verbatim (kev_required_action, not EchelonGraph's advice; kev_short_description; kev_notes_urls), ghsa_id, references, cpe_match (one flat list) and cpe_configurations (NVD's configurations as NVD sent them, with each AND/OR operator; absent where none is stored, which is not a finding that no product is affected), remediation (a capped summary of its fix), published, modified and updated_at; each field only where the record has it. Pass a CVE, EUVD or GHSA ID. 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; score_unassessed_reason says why, and the note labels each such CVE NOT YET SCORED. 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. patch_available is true when EchelonGraph holds, or once held, fix evidence for the CVE; patch_evidence is what it holds now, strongest first. patch_available false means no fix evidence is on record, which is not a finding that no fix exists. freshness is null: this answer carries no poll-completion time. Past 30,000 characters of JSON, the first text block holds data cut to fit, and the note says what the cut leaves out and where to read it (TEXT CUT); so is structured data (DATA CUT), unless full: true. Cut, each list in the record keeps its first entries, and the note names each list cut with its full length.
Inputs: cve_id (required), full. Returns: object
get_cweCWE and its CVEsARead-onlyIdempotentInspect
One CWE (weakness class) and the CVEs classified under it. Returns cwe_id, name and description (from the MITRE CWE catalog EchelonGraph embeds, version catalog_version), total (the active CVEs in EchelonGraph's feed that an NVD, GitHub or CVE.org record classifies under it, rejected and reserved records left out), and cves, one page of 50: each with cve_id, severity, cvss_v3_score, echelongraph_score, echelongraph_severity, score_assessed, kev_listed, published and a shortened description. order says how the rows are sorted (CISA-KEV-listed first, then EchelonGraph score); an answer without order does not state its order, and the note says so. page is the page served, page_size its size and max_page the last page the API serves: a later page is answered as max_page, so past max_page pages total counts CVEs no page lists. echelongraph_score is EchelonGraph's score only when score_assessed is true; the note labels each row that is NOT YET SCORED. A total of 0 says that no CVE in EchelonGraph's feed is classified under that CWE, not that none exists. Pass cwe_id like CWE-79 (or 79) and an optional page (1-200). Its structured result carries state (measured), measured_at (null), method, coverage (total, returned, page, page_requested, page_size, max_page), freshness (null) 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. Past 30,000 characters of JSON, the first text block holds data cut to fit, and the note says what the cut leaves out and where to read it (TEXT CUT); so is structured data (DATA CUT), unless full: true.
Inputs: cwe_id (required), full, page. Returns: object
get_vendor_advisoryVendor advisory detailARead-onlyIdempotentInspect
One vendor advisory in full, by vendor and the vendor's advisory ID (the vendor and vendor_advisory_id of a vendor advisory row, never its advisory_id, EchelonGraph's internal UUID): title, description, severity, cvss_v3_score, cve_ids and known_cve_ids (those with a record in EchelonGraph's CVE feed), affected_products, remediation, references, vendor_modified_at, and withdrawn_at and withdrawn_reason when the vendor withdrew it. remediations lists each remediation the vendor lists, with kind (the vendor's own category, never read from the text: vendor_fix, workaround, mitigation, no_fix_planned, none_available or other), source_category, text, url and the cve_ids and product_ids it covers; each text is the vendor's, verbatim, untested by EchelonGraph. remediation_state is parsed, none_in_source, not_parsed or withdrawn: none_in_source and not_parsed are not a finding that no fix exists. Each advisory carries vendor_published_at (the vendor's date), our_first_seen_at (when EchelonGraph first recorded it) and withdrawn (true: the vendor rescinded it). Each advisory also carries rejected_cve_ids: the CVE IDs it names whose CVE record was rejected (withdrawn) by its numbering authority, which are not active vulnerabilities. coverage.vendor_window is that vendor's window in what EchelonGraph holds: vendor, advisories, earliest_vendor_published_at, latest_vendor_published_at, held_since (where that window begins), history_backfill (in_progress or not_started: older advisories not all held yet), history_scope and feed_held_since; null when the windows could not be read or carry none for that vendor. measured_at is our_first_seen_at. Its freshness is null: the answer carries no poll-completion time. Past 30,000 characters of JSON, the first text block holds data cut to fit, and the note says what the cut leaves out and where to read it (TEXT CUT); so is structured data (DATA CUT), unless full: true.
Inputs: vendor (required), advisory_id (required), full. Returns: object
kev_recentRecent CISA KEV additionsARead-onlyIdempotentInspect
The CVEs CISA has added to its Known Exploited Vulnerabilities (KEV) catalog, newest first, from EchelonGraph's copy of that catalog, which polls CISA's feed every 5 minutes. Each kev row gives cve_id, kev_added_date (CISA's dateAdded), kev_due_date, kev_vendor, kev_product, kev_vuln_name and kev_ransomware (known ransomware-campaign use), EchelonGraph's severity, cvss_v3_score, epss_score, epss_percentile and eg_kev_tier for the CVE, and our_first_seen_kev, when EchelonGraph's poller first recorded the CVE entering the catalog (null where no such record exists). Filter by since and until (YYYY-MM-DD, inclusive, on kev_added_date), ransomware, and vendor (an exact, case-insensitive match on kev_vendor); page with limit (1 to 200, default 50) and cursor, passing back the previous page's next_cursor with the same filters. total counts the CVEs matching the filters; kev_listed_total counts every CVE EchelonGraph holds as KEV-listed, and catalog.catalog_count is CISA's own count in the catalog last fetched, so a gap between the two is entries not yet in EchelonGraph's CVE table, which no page returns. CISA's requiredAction, shortDescription and notes are not in these rows: each CVE's own record carries them, verbatim and labelled as CISA's. Its structured result's measured_at is our last successful fetch of CISA's feed (null when the API does not say), coverage counts the CISA KEV catalog (total, returned, kev_listed_total, catalog_count, limit, has_more) and freshness gives last_successful_fetch_at, catalog_version and date_released. Past 30,000 characters of JSON, the first text block holds data cut to fit, and the note says what the cut leaves out and where to read it (TEXT CUT); so is structured data (DATA CUT), unless full: true. Cut, each row keeps cve_id, kev_added_date, kev_vendor and kev_ransomware at least, or rows are left out, and next_cursor still continues after the page's last row, not the text's.
Inputs: full, limit, since, until, +3 more. Returns: object
scan_manifestCheck a lockfile against the advisory corpusARead-onlyIdempotentInspect
Check a project's lockfile or pinned manifest against EchelonGraph's advisory corpus, one verdict per dependency. Pass files: 1 to 20 of filename and content, up to 5,000,000 characters in all. Read: requirements.txt (== and === pins only), go.mod (after replace and exclude), package-lock.json and npm-shrinkwrap.json (v1-v3), Cargo.lock, Gemfile.lock, composer.lock, poetry.lock and gradle.lockfile. package.json, pyproject.toml, Pipfile, Gemfile, Cargo.toml, composer.json and build.gradle hold ranges and are refused, naming the lockfile; go.sum is refused: it lists versions the build does not select. The server reads the files into purls; only the purls are sent to the API, in POST bodies of at most 200, never in a URL; filenames and file contents are not sent on. Run from npm, this server is on your machine; over the hosted endpoint (mcp.echelongraph.io) it is EchelonGraph's, and the files are the request body, up to 6 MiB. data.results holds a row per distinct purl, with verdict (affected, not_affected, undetermined or not_assessed) and cve_ids; each match carries kev_listed, epss_score and fixed_in. not_affected is the only clean verdict. data.not_checked lists the first 2,000 entries not sent (file, line, reason), counting all; not_checked entries are not clean. Refused: over 40,000 lines or 300,000 JSON/TOML values a call, a filename over 1,024 characters. At most 2,000 distinct purls and 50 seconds a call. The API allows 1,200 components a minute per caller; on a 429 it waits out Retry-After within that; a later batch cut off, refused or failed ends the call, the rest in data.not_sent_purls, not clean. Past 30,000 characters of JSON, the first text block holds data cut to fit, and the note says what the cut leaves out and where to read it (TEXT CUT); so is structured data (DATA CUT), unless full: true. Cut, each row keeps index, purl, verdict, not_assessed_reason and cve_ids at least; not_checked its first 20.
Inputs: files (required), full. Returns: object
search_cvesSearch CVEsARead-onlyIdempotentInspect
Search/list CVEs from EchelonGraph's CVE feed (NVD + MITRE-CNA pre-NVD + CISA-KEV + EPSS + GitHub GHSA, each polled on a schedule). Filter by severity, minimum CVSS, free text, and sort; page with limit and offset. Returns cves, each with cve_id, severity, cvss_v3_score, echelongraph_score and score_assessed (whether EchelonGraph has scored it), epss_score and kev_listed where the record has them, and the list's total, total_counted (false: total is not a count), total_is_lower_bound (true: at least total), search_relaxed, limit and offset. 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; score_unassessed_reason says why, and the note labels each such CVE NOT YET SCORED. 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. patch_available is true when EchelonGraph holds, or once held, fix evidence for the CVE; patch_evidence is what it holds now, strongest first. patch_available false means no fix evidence is on record, which is not a finding that no fix exists. freshness is null: this answer carries no poll-completion time. Past 30,000 characters of JSON, the first text block holds data cut to fit, and the note says what the cut leaves out and where to read it (TEXT CUT); so is structured data (DATA CUT), unless full: true. Cut, each row keeps at least the fields named above and the first 200 characters of its description (100 on a page too long for that), or rows are left out and the note gives the offset to call next.
Inputs: full, sort, limit, offset, +3 more. Returns: object
search_vendor_advisoriesSearch vendor advisoriesARead-onlyIdempotentInspect
Search or list vendor-published advisories, newest first. A query of 3 or more characters is matched case-insensitively as a substring of each advisory's title, description, vendor name, IDs and affected_products (search_match substring). A query of 1 or 2 characters matches whole words only (search_match word). Filter by vendor (slug), severity band and whether the advisory names any CVE ID. Advisories their vendor withdrew are left out. Returns advisories, each with vendor, vendor_advisory_id, title, severity, cvss_v3_score, cve_ids, summary and affected_products where present, and total, total_capped, limit, offset, search_applied and search_match. 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. Each advisory also carries rejected_cve_ids: the CVE IDs it names whose CVE record was rejected (withdrawn) by its numbering authority, which are not active vulnerabilities. The query is sent in a request header, never in the URL. coverage repeats those beside returned, and gives vendor_windows, each vendor's window in what EchelonGraph holds: vendor, advisories, earliest_vendor_published_at, latest_vendor_published_at, held_since (where that window begins), history_backfill (in_progress or not_started: older advisories not all held yet), history_scope and feed_held_since. An advisory published before its vendor's held_since may not be held, so no advisory from a vendor is not a finding that it published none. Past 30,000 characters of JSON, the first text block holds data cut to fit, and the note says what the cut leaves out and where to read it (TEXT CUT); so is structured data (DATA CUT), unless full: true. Cut, each row keeps vendor, vendor_advisory_id, title, severity and cve_ids at least, its strings and lists shortened, or rows are left out and the note gives the offset to call next.
Inputs: full, limit, query, offset, +3 more. Returns: object
vendor_advisories_for_cveVendor advisories for one CVEARead-onlyIdempotentInspect
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 given. Covers the vendor feeds EchelonGraph polls, for example Microsoft MSRC, Red Hat, Cisco, Palo Alto Networks and GitHub GHSA; an empty answer means none of the advisories EchelonGraph holds names the CVE, not that no vendor published one. Each advisory carries vendor_published_at (the vendor's date), our_first_seen_at (when EchelonGraph first recorded it) and withdrawn (true: the vendor rescinded it). Each advisory also carries rejected_cve_ids: the CVE IDs it names whose CVE record was rejected (withdrawn) by its numbering authority, which are not active vulnerabilities. The answer's cve_rejected is true when the CVE record of the CVE asked for was rejected. Each row has remediation_kinds and remediation_state (not_parsed is not none). coverage gives returned, cap, at_cap (true: may be more) and cve_year; vendor_windows, each vendor's held window: vendor, advisories, earliest_vendor_published_at, latest_vendor_published_at, held_since (where that window begins) and history_backfill (in_progress or not_started: older advisories not all held yet); and vendors_not_fully_held, the vendors absent from the answer of which EchelonGraph holds none, whose held_since is after 1 January of cve_year or not known (outside history_scope, feed_held_since), or whose history is still being read. An advisory published before its vendor's held_since may not be held, so no advisory from a vendor is not a finding that it published none. Past 30,000 characters of JSON, the first text block holds data cut to fit, and the note says what the cut leaves out and where to read it (TEXT CUT); so is structured data (DATA CUT), unless full: true. Cut, each row keeps vendor, vendor_advisory_id, title, severity and cve_ids at least, its strings and lists shortened.
Inputs: cve_id (required), full. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
TDQS
Scored across 16 tools
Two clusters of overlap exist: cve_intel, cve_remediation, get_cve and vendor_advisories_for_cve all return fix/affected-version evidence for the same CVE, and check_affected, check_sbom and scan_manifest all answer 'what is affected' with different input shapes. The very long descriptions do explain the boundaries (single component vs purl list vs lockfile; enrichment capsule vs remediation narrative), which prevents total confusion, but an agent asking 'how do I fix CVE-X' still has four plausible entry points.
All names are snake_case and entity-oriented, with a recognizable get_/search_/check_/scan_ verb prefix family. The deviation is that single-record lookups are split between prefixed forms (get_cve, get_cwe, get_vendor_advisory) and bare noun forms (cve_summary, cve_intel, cve_exposure, cve_remediation, kev_recent, epss_history), which reads slightly arbitrary but remains predictable.
Sixteen tools is at the upper edge but each maps to a distinct surface of the product: CVE lookup/enrichment, vendor advisories, KEV, EPSS, CWE, exposure radar, and three dependency-scanning entry points. Nothing appears redundant enough to cut outright; the surface is reasonable for a vulnerability-intelligence platform.
Core workflows are covered end to end: discover CVEs (search_cves, kev_recent, cve_summary), inspect them (get_cve, cve_intel, cve_exposure, epss_history, get_cwe), find advisory/remediation detail, and assess your own components (check_affected, check_sbom, scan_manifest). Minor gaps: no CWE catalog browsing/search (only get_cwe by ID), no filter on CVEs by KEV/EPSS in search_cves, and no vendor listing or browsing endpoint beyond advisory search.
Maintenance
Related MCP Connectors
Read-only vulnerability intelligence: CVEs, exploitation evidence, fixes and product matches.
Free, read-only public-source threat intel over MCP. No account or API key.
CVE intelligence: exploitation (KEV/EPSS), detection coverage, fixed versions, CPE configurations.
Is a CVE exploited? CISA KEV, FIRST EPSS, CVSS and SSVC with a patch-first verdict. Free, no key.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceProvides keyless, read-only access to CISA KEV, SSVC, and full ICS advisory corpus, letting users check CVE statuses, compute BOD 26-04 remediation timelines, and search advisories by vendor, product, CVE, CVSS, or sector.351 npm1Apache 2.0
- FlicenseNot gradedqualityAmaintenanceEnables querying the CVE-Search API to browse vendors, list products for a vendor, retrieve vulnerabilities for a vendor and product, fetch individual CVE records, get the latest CVEs with CAPEC/CWE/CPE expansions, and inspect current database version information. This gives MCP clients direct, structured access to public vulnerability data.-
- FlicenseNot gradedqualityAmaintenanceEnables querying the CVE-Search API to retrieve vendors, products, product-specific vulnerabilities, specific CVE IDs, recently updated CVEs with CAPEC/CWE/CPE expansions, and database update information. It supports natural-language access to CVE data through MCP clients.-
- AlicenseAqualityBmaintenanceEnables any MCP client (Claude Code, Claude Desktop, Codex, or a custom agent) to fetch the latest CVEs at query time from live sources — NVD, CVE List V5, GitHub Advisory Database, CISA KEV and FIRST EPSS — without a separate API key. Exposes search, filtering (severity, RCE, PoC, KEV, keyword, CPE), per-CVE detail and risk-scored patch prioritization as JSON tools, so answers are grounded in live data rather than model memory.61MIT