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/mcp - Source: https://github.com/echelongraph/echelongraph-mcp · Changes: CHANGELOG.md · Security: SECURITY.md
- Also listed on: Smithery · 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. No Node.js, no PATH and no
JSON to edit. 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.
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-mcp
Add --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/mcp
Keyless, 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/mcp -
Cursor (
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. What it sees and logs is under "Privacy",
and its per-client limit under "Rate limits".
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.
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 and history_backfill. held_since is the start of the window EchelonGraph has read the
vendor's advisories from: one published before it may not be held. For a vendor with no history read (history_backfill
not_supported) it can be years after the earliest advisory held: Cisco's poller first read the
advisories Cisco had changed in the last 90 days, Red Hat's and GitHub's the last 7, so an older
advisory the vendor revised later is held while the ones published beside it are not.
vendor_advisories_for_cve lists in vendors_not_fully_held each vendor with no advisory in the
answer of which EchelonGraph holds none, whose held_since is after 1 January of the CVE ID's year
or not known, or whose history is still being read: no advisory from such a vendor is not a finding
that it published none.
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.
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):
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, 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.
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_cvefor CVE-2021-44228 was 65,540 characters ofstructuredContenton production's answer of 2026-10-11, beside a first text block of 23,759, and Claude Code saved it to a file. Since 3.0.0 the structured result 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 index, purl, verdict, not_assessed_reason, cve_ids,
matched_package and matched_via, and per match cve_id, kev_listed, ransomware, epss_score,
effective_score, score_assessed, fixed_in and fixed_in_reason, every field a prompt reads from
a match, then each row's matches to at most the same
number, its first, a row so cut carrying matches_truncated (its total, and the KEV-listed and
ransomware CVE IDs and fixed_in versions of all its matches), then each match to cve_id,
kev_listed, ransomware, fixed_in and effective_score, then each row to index, purl,
verdict, not_assessed_reason and cve_ids (rows leave at the compact level; the rows kept
take back detail in order); 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 cut not_checked, then not_sent_purls, whole or none (the note says how to
get it), then drop the not_affected rows,
then the not_assessed ones ( counts every verdict), and only then affected rows,
from the end; drops its and samples first and its
, which names every match, last; the other row tools drop rows from the end, and those that
page say the or to read them from ( and
also the page size whose rows fit whole, counted on the rows whole). cuts
and , each to at most the same number of entries, then
; cuts its daily and weekly series, each keeping its newest
entries, then every ranking, then ; then any other list at the top of
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 still validates;
a cut adds only and ). The result
then says ( on every other success), gives each list cut,
by its path in , with its , the number and, where its entries are thinned,
, and the note ends with sentences starting that say what was cut, how to
read the rest and that the same call with returns whole.
The first text block. Through 2.6.2 it was the API's JSON whatever its size: on production's
answers of 2026-10-04 a default search_cves page for "openssl" was 133,300 characters of text,
get_cve for CVE-2021-44228 80,897, check_affected for the Linux kernel at 5.10.0 364,408,
kev_recent at its largest page 105,925 and search_vendor_advisories at its largest page
103,147. 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 field the first level keeps, and 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, 15 of 200 beside 50 undetermined, and the note
says how many), 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. 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 the
note says how to read them: the offset to call next, or a smaller . Any other answer
keeps the first entries of each of its lists ('s and ). The note
then ends with one sentence starting that says what the first text block leaves out
and which tool returns a row (, , , and
a CVE's fixed versions): whole in that tool's with
, 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
resource reads 's record whole and cuts its as 's first
text block is cut, and says so: , naming each list it cut, and a
note starting . The prompts ask only for what the
text keeps: reads 50 rows a page, which the text holds whole;
reads CISA's due date from ; and reads each match's ,
and 's where the text keeps no match for a component.
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 (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 of the answer (see "Long answers"), and cannot disagree with it;
beside a DATA CUT, the first text block is cut from the answer whole, not from the cut data. Before 2.2.0 this block repeated the whole note, so every note was sent twice.
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 what they leave out.
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. 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; 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 length at which
Claude Code cuts each of them before the model reads it (its CLAUDE_CODE_MAX_MCP_DESCRIPTION_LENGTH,
default 2048), 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:
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 and
path, the status, the duration, the body size, the JSON-RPC method and (for tools/call) the
tool name, the protocol version, whether an Origin header was sent, and the family of your
client's User-Agent. 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, which EchelonGraph's server reads into purls in
memory, sending only those to the API, and neither logs nor keeps the files. To keep lockfiles on
your machine, run the npm package. When a client passes 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's Retry-After within
50 seconds a call, and answer what they have, the rest counted in coverage.not_sent. The hosted
endpoint adds 120 MCP requests a minute per client address, and refuses a request body over 64 KiB
(6 MiB for a check_sbom or scan_manifest call, or an sbom_review prompt). Every limit, path
by path, with what a tool answers past each:
docs/rate-limits.md.
The API's own documentation of these limits: https://echelongraph.io/pulse/api.
Configuration
The npm package (stdio):
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 container with
little memory, start it with V8's heap capped below the container's limit, as EchelonGraph's
hosted deployment does in 256 MiB (node --max-old-space-size=104 --max-semi-space-size=16 dist/http.js): Node
otherwise sizes its heap from the memory the system reports, which in a container can be the
host's, and the container is then killed before V8 collects.
It reads the three variables above and:
Develop
npm install
npm run build # tsc → dist/
npm test # build, then the behavioural suite over both protocol eras, against a stub API (no network needed)
npm run smoke # spawn the server + call cve_exposure against the production API
npm run smoke calls the production API with this package's User-Agent. Run from our own
workspace, whose egress address the API recognises as ours, its requests are counted as our own
traffic and appear in no adoption count. Run from any other machine, they are counted as external
MCP adoption unless you set ECHELONGRAPH_MCP_UA (for example
ECHELONGRAPH_MCP_UA=my-smoke/1.0 npm run smoke), which files them under that token's family
instead. 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.
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.0.2 a success's outputSchema admits top-level fields it does not name, so a field added
in a minor release does not fail a client still holding the tool list it read before. A client
that shows "does not match the tool's output schema" after an upgrade is validating against the
schema of the version it listed: reconnect it (Claude Code: /mcp, then reconnect echelongraph) or restart it. Each tool's data
schema stays closed: a field added inside it needs that reconnect too.
Security
Report a vulnerability privately to [email protected], 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.
