Repository navigation
Releases: echelongraph/echelongraph-mcp
Release list
echelongraph-mcp 3.0.0
Issue numbers (#NNNN) refer to EchelonGraph's internal issue tracker, which is not public: they are not issues of the public repository, and cannot be followed from it.
A major version (README, Versioning): the structured result is now held to 30,000 characters by
default (the first entry below), so a program that read structuredContent.data whole gets it cut
on a large answer unless it passes full: true. A client that hands the model the text or the
structured result needs no change; it gets an answer where 2.7.x gave Claude Code a file path.
-
fixed_inis never a version the same CVE still affects (#3074).fixed_in, a match's in
check_sbom, scan_manifest and check_affected and cve_remediation's for an installed version, is
the first version above the installed one that no affected range of the advisory holds: the
fixed bound of the range holding it, moved past any overlapping range of the same record.
CVE-2026-4800 for lodash 4.17.15 answeredfixed_in4.17.21, the bound of[0, 4.17.21), while
[4.0.0, 4.18.0)on the same row still holds 4.17.21; it now answers 4.18.0, and after the #3075
re-import the row holds only[4.0.0, 4.18.0), CVE-2026-4800's own range. When the bound moved, the API's
fixed_in_detailnames the ranges it moved past. The API applies the rule; the server relays
it. cve_intel carries nofixed_in: its description, itsfixed_branchesschema description
and its note now state the same walk forfixed_branches(an affected version's fix is the
firstfixedabove it that no range of its row holds, none past a range without one), where
its description had said thefixedof the range holding the version is its fix. -
A GitHub advisory's ranges go to the CVEs GitHub names for them (#3075). Where GitHub's
attribution can be read (its own advisory record, then its one NVD advisory reference, then a
single CVE alias), a GHSA that OSV merged into a group of several CVEs no longer lends its ranges
to the others. A record none of these settles keeps its alias set, as before, so no CVE loses a
range it owns. Matches that came only from a range another CVE owned leave
check_sbom's, scan_manifest's and check_affected's answers. -
cve_remediationcarriesfix_status, the API's fix-status rule that CVE Pulse pages show
(available,fix_referenced,fix_merged,none_confirmedorunknown, with itsbasis),
and get_cve'sremediationsummary carries it first. Both relay it; the server derives nothing
(#2841). The rule reads the CVE record's own fix statement (#3076): a fix the record's solutions
state isavailablewithbasiscna_solutionandsolution_by(the sentence,statement,
and the CNA that wrote it,cna); a fixpatch_evidencecounts from the record with no sentence
to quote isavailablewithbasispatch_evidence_statementandstated_in_record; and
unread_inputscan namecna_record. -
patch_evidencehas three more labels (#3076, #2518; core-backend):vendor_advisory_fix(a
vendor advisory listing a fix for the CVE under the vendor's own structured category: Red Hat's
CSAFvendor_fix, Microsoft's Vendor Fix),cna_solution(a sentence of the CVE record's
solutions that names a fixed version) anddescription_fix(a sentence of the CVE description
that does). get_cve's outputSchema names all eight. -
get_cve relays what the CVE record's CNA wrote (#3070, #3076; core-backend):
cna_solution, the
sentence of its solutions that names the fixed release, verbatim;date_public, when the
vulnerability was made public as the CNA states it (date_public_sourcecna), which is not the
record's ownpublished; andcna(short_name,name). The note names each as the CNA's
where the record carries it. -
get_cve's note gives
score_factors.bod_26_04where the scorer wrote it (algorithm v3.4, #2880):
CISA BOD 26-04's remediation timeline for the CVE if the affected system is publicly exposed and
if it is not, with each input and its source. Its outputSchema describes
echelongraph_ssvc_decisionandscore_factors. -
CISA's required action is attributed to BOD 26-04, which replaced BOD 22-01 on 10 June 2026
(#3084): get_cve's note and itskev_required_actiondescription, and cve_remediation's note and
description. -
cve_remediationtakes the installed version:ecosystem,packageandversion, all three
or none, sent in theX-EG-Ecosystem,X-EG-PackageandX-EG-Versionrequest headers and
never in the URL. It then answersinstalled(that version's verdict against the CVE),
fixed_inand, when it is null,fixed_in_reason; that answer is private andno-store.
Fix tags on references match in either case (#2841, #3073). -
The
sbom_reviewprompt reads a fixed version fromfixed_inalone, and names the source
package where a row matched upstream (matched_viaupstream,matched_package) (#2834, #2836). -
README, Claude Desktop: the "check that the file parses" step prints one command per OS with the
config path filled in ("$HOME/Library/Application Support/…"in Terminal,
"$env:APPDATA\Claude\…"in PowerShell). Through 2.7.2 it took a quoted"<path to the file>",
and the macOS path pasted into it (~/…) is not expanded inside quotes, so a valid file answered
ENOENT(#3096). -
README: fits npm's 64 KiB with its Privacy and License sections; the long reference moved to
docs/tools.md,docs/exposure-radar.md,docs/structured-results.mdand
docs/rate-limits.md, linked from it. Every count it states is held by a test (#3012, #2922). -
Contract change: the structured result is held to 30,000 characters by default; pass
full: truefor its data whole. Claude Code hands the modelstructuredContent, not the text
blocks, and refuses one pastMAX_MCP_OUTPUT_TOKENS(25,000 by default), saving it to a file:
get_cvefor CVE-2021-44228 was 65,540 characters ofstructuredContenton production beside a
first text block of 23,759, so the model got a file path, not an answer. Through 2.7.x
structuredContent.datawas always the answer whole. Now, when the whole structured result
(envelope, notes and data, as compact JSON) would pass 30,000 characters, its data is cut, in an
order each tool states. Where one row is large, every row is first thinned to some of its fields,
all rows kept, before any row leaves: check_sbom's and scan_manifest's rows toindex,purl,
verdict,not_assessed_reason,cve_ids,matched_packageandmatched_via, each match to
cve_id,kev_listed,ransomware,epss_score,effective_score,score_assessed,
fixed_inandfixed_in_reason, every field the prompts read from a match (all 50 rows of the
Juice Shop 11.1.2 SBOM stay); check_affected's matches to their scores, flags, criteria and
fixed_in, then fewer; cve_remediation's advisories without each item'sproduct_ids, then
without its text, the note then namingget_vendor_advisoryas where each text is;
search_cves' rows to their description and scores, then the scores; the vendor-advisory tools' rows
withoutaffected_products, then withoutsummary. Then lists leave entries: get_cve's
cpe_matchandreferencesfirst; search_cves', kev_recent's, get_cwe's and the vendor-advisory
tools' rows from the end; check_affected'sexcludedandundeterminedsamples, then its
matches,cve_idslast; check_sbom's and scan_manifest'snot_sent_purlsandnot_checked, then
theirresults, thenot_affectedrows leaving first, then thenot_assessedones, and only then
affected rows, from the end; cve_remediation'svendor_remediations, thenpatches;
exposure_radar's daily and weekly series, each keeping its newest entries, then its rankings,
thenkev_exposure.newest_kev; then any other list at the top of data. The room left once a
list fits goes back to the lists emptied before it, as many of their entries as fit, their first
or a series' newest (scan_manifest'snot_checkedbeside 64 thinned rows). 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 against the
outputSchema. Every success now carriesdata_truncated(false when data is whole), and, when it
is true,truncated: each list cut, by its path in data, withtotal,returnedand, where its
entries are thinned,thinned: true; the note ends with sentences startingDATA CUTthat say what
was cut and to which fields, how to read the rest (the offset, limit or page size, where the tool
pages; a page size said to fit data whole is counted on the rows whole) and that the same call
withfull: truereturns data whole. Every tool takesfull
(boolean, optional). A program that relied on data whole must passfull: true. The first
text block is cut from the whole answer exactly as before (TEXT CUT); beside a cut data its TEXT CUT
sentence says the answer is whole instructuredContent.datawithfull: true, and so does each
sentence that points to another tool's data (#3013). -
check_sbom's and scan_manifest's first text block keeps each match's
fixed_in_reasonbeside
fixed_inat every cut level that keeps matches. Their note, and thesbom_reviewand
workload_triageprompts, saydata.not_sent_purlslists the purls not sent whole with
full: true, since a DATA CUT can cut that list; the position the note gives still rebuilds it.
scan_manifest's note says the same ofdata.not_checked(#3013). -
The
triage_cveandsbom_reviewprompts read a vendor's workaround or mitigation step from
get_vendor_advisorywhere cve_remediation's note says its data or text leaves that item's text
out or shortens it (#3013). -
scan_manifest answers a file of very many entries: it read each file's entries into its lists as
the arguments of one call, and a...
echelongraph-mcp 2.7.2
Issue numbers (#NNNN) refer to EchelonGraph's internal issue tracker, which is not public: they are not issues of the public repository, and cannot be followed from it.
- exposure_radar flags every radar whose last completed check is past its threshold, not only the
two whose answer carries a liveness verdict. kev_exposure and exposed_databases (12-hour cycles)
are flagged after 24 hours, leaked_credentials (a 10-minute cycle) after 1 hour; shadow_ai and
mcp_servers keep the API's own poller.running and enabled verdicts, and fall back to 30 and 45
minutes only where the answer gives none. A flagged radar's sentence gives the age and the cycle
and says its numbers may be stale. The age is taken against the clock when the answer is
composed, never the stats cache's generated_at. The closing sentence "Each radar refreshes on its
own schedule." is gone: it said the radars were refreshing while four of the five were stopped.
It now lists the radars that may be stale, or names each threshold when none is. A test reads
each radar's cadence from core-backend and fails when a threshold is under twice it (#3010). - README: the freshness sentence says a radar refreshes only while it runs (#3010). The setup steps
say to quit the client fully and reopen it, and no longer give a tool count to look for:npx
runs the newest release and a connector the hosted endpoint's, so a number could differ from a
working install's and read as a failure (#2813). - cve_intel: each exploits row carries
source_published_at, the source's own date for the
reference (null where the source gives none), andeg_first_seen_at, when EchelonGraph recorded
it.first_seen_atis kept, deprecated, assource_published_atunder its old key (#2876).
echelongraph-mcp 2.7.1
Issue numbers (#NNNN) refer to EchelonGraph's internal issue tracker, which is not public: they are not issues of the public repository, and cannot be followed from it.
- README: the server serves five prompts, not four;
workload_triage(added in 2.7.0) is now in
the intro and in the Prompts table, and a test holds the README's prompt table and every
"N prompts" count equal toprompts/list(#2922). - Every tool description now keeps at least 100 characters under the 2,048-character cut some
clients apply. check_affected, check_sbom, vendor_advisories_for_cve, get_cve, scan_manifest,
kev_recent, cve_intel and cve_summary were reworded for length alone: no field, verdict or
not-assessed statement was removed. A test holds the headroom (#2924). - Hosted endpoint:
mcp_handler_errorlog lines now carryerror_class, a label from a fixed list
matched against the SDK's own error messages, and, for a protocol error, its numeric
error_code, so each can be told apart. The message itself is still never logged, as it can
quote what a client sent; /privacy Section 11 says so (#2923).
echelongraph-mcp 2.7.0
Issue numbers (#NNNN) refer to EchelonGraph's internal issue tracker, which is not public: they are not issues of the public repository, and cannot be followed from it.
-
New tool scan_manifest (#2835): a project's lockfiles or pinned manifests checked against the
advisory corpus, one verdict per dependency, in one call. Passfiles(1 to 20 offilenameand
content, up to 5,000,000 characters in all). This server reads them into purls and sends only
the purls, through the same batch route, batches of 200, 50-second budget,Retry-Afterwaits
and cancellation as check_sbom (that loop now lives insrc/tools/match_batch.ts, shared by both
tools, check_sbom's behaviour unchanged). Read: requirements.txt (==and===pins only),
go.mod (requirewithreplaceandexcludeapplied), package-lock.json and
npm-shrinkwrap.json (lockfileVersion 1 to 3), Cargo.lock (v1 to v4), Gemfile.lock,
composer.lock, poetry.lock and gradle.lockfile, each tested on files written by the ecosystem's
own tool. Every entry not sent is listed indata.not_checkedwith its file, line and reason
(version_unpinned,version_unresolved,local_path,vcs_source,unsupported_line) and is
not clean:django>=4is never sent aspkg:pypi/django@4. package.json, pyproject.toml,
Pipfile, Gemfile, Cargo.toml, composer.json and build.gradle are refused with the lockfile to
pass; go.sum is refused, as it lists versions the build does not select. No fixed version is
derived: each match'sfixed_inis the API's. TOML is read by a small reader in this package
(src/tools/manifests/toml.ts), so no runtime dependency is added. 16 tools. -
The hosted endpoint admits a
tools/callof scan_manifest up to 6 MiB, as it does check_sbom
(LARGE_BODY_TARGETSnow lists names per method); the files are read in memory and neither
logged nor kept. The 413 and 503 texts name no single tool. -
check_sbom's outputSchema names
kev_listedandepss_scoreon each match, which the API
already sent. -
New tool cve_remediation (#2841): how one CVE is fixed, as its sources state it, in one answer:
CISA's KEV text (its required action is directed at US federal civilian agencies), every
affected range with its fix, each vendor advisory's remediation categorised by the vendor's own
category (vendor_fix, workaround, mitigation, no_fix_planned, none_available, other), the
references NVD tagged Patch or Mitigation, and patches. Nothing is generated; every text is its
source's, untested by EchelonGraph. none_in_source, not_parsed and an empty list are not a
finding that no fix exists; a REJECTED CVE answers not_assessed. 15 tools. Its text for
CVE-2021-44228 on production's data is 20,644 characters (assembled before the route was
deployed; re-measure live). -
get_cve relays remediation, a capped summary (at most 1,500 characters of JSON) of the same.
-
vendor_advisories_for_cve rows carry remediation_kinds and remediation_state, and
get_vendor_advisory carries remediations, each item with its kind, verbatim text and URL
(#2840). Red Hat, Microsoft and Palo Alto are parsed; other vendors are not_parsed. -
vendor_advisories_for_cve, get_vendor_advisory and search_vendor_advisories now say when an
advisory names a CVE ID whose CVE record was rejected (withdrawn) by its numbering authority
(#2865). Through 2.6.8 such an advisory read as a normal one when its vendor had not withdrawn
it: measured in production on 2026-10-05, 767 single-CVE advisories that their vendors had not
withdrawn named a CVE that EchelonGraph holds as REJECTED (GitHub 740, Microsoft 25, Red Hat 2),
andvendor_advisories_for_cvefor CVE-2026-7936 returned Microsoft's "Chromium: CVE-2026-7936
Object lifecycle issue in V8" with nothing saying the CVE was rejected. core-backend now serves
rejected_cve_idson each advisory: the CVE IDs it names that were rejected,[]when none
was, null when that could not be checked. vendor_advisories_for_cve also getscve_rejected
for the CVE asked for. The vendor's ownwithdrawnis unchanged: it still says only whether the
vendor rescinded its advisory. The note opens withCVE REJECTEDfor a rejected CVE, names each
advisory that names a rejected CVE ID, and says when the state could not be checked. The three
descriptions and the outputSchema describe both fields. When a row is cut to fit the text, it
keepsrejected_cve_idsat both cut levels. -
GitHub advisories whose text is a CVE rejection notice ("Rejected reason: …") are now served
withwithdrawn: true(#2864; core-backend). search_vendor_advisories leaves them out, as it
does every withdrawn advisory. -
get_cve relays CISA's own KEV text for a KEV-listed CVE (#2839; core-backend):
kev_required_action(CISA's requiredAction),kev_short_descriptionandkev_notes_urls
(the https URLs in CISA's notes, at most 10), alongsidekev_due_date, which the API already
served. Through 2.6.8 none of the three was stored. The note says the text is CISA's, under BOD
22-01, and not EchelonGraph's advice. triage_cve quotes the required action, attributed to CISA. -
patch_evidencehas two more labels (#2889, #2890; core-backend):release_reference: EchelonGraph's GitHub release, PR or commit reading found a published,
non-draft release that contains the fix.cna_patch_reference: the CVE's numbering authority tagged one of its referencespatchin
its CVE record.
Measured in production on 2026-10-05, 6,395 CVEs held one of these and read
patch_available: false. Among them were CVE-2026-97724, whose fix release worklets-0.12.2
EchelonGraph had read, and CVE-2026-69486, which Microsoft taggedpatch. A reference that is
only marked from page wording, such as an advisory page that mentions a patch, is still not
counted as evidence. -
epss_history now says where its record becomes complete (#2867; core-backend). Through 2.6.8
it presented 2026-05-18 onward as a complete record. Measured on 2026-10-05, every FIRST score
date in that span had been applied. But the old write path dropped many per-CVE changes within
a night's refresh: on 2026-09-21, 202 of 300 sampled CVEs whose EPSS changed got no history row.
The answer now carries:complete_since: 2026-10-04 23:14:54Z, the first refresh of the new write path;series_complete;- a
score_dateon each point; - a coverage block of days held, partial and missing. It comes from a new per-refresh run log,
so it is exact from the deploy onward.
The note names the incomplete stretch. It never calls the record complete without
complete_since. -
check_sbom follows a Debian or Alpine purl's
upstream=qualifier to the source package it was
built from (#2836; core-backend). Advisories are keyed by source package. Measured on
2026-10-05, Debian 12 holds 0 rows forlibssl3and 262 foropenssl. Through 2.6.8,
pkg:deb/debian/libssl3@…?upstream=opensslandpkg:apk/alpine/libcrypto3@…came back
not_assessed, while the same version underopensslwas affected. Each row now says
matched_packageandmatched_via(purl_name,upstreamorcoordinates), and a match found
throughupstream=says so in itsmatch_reason. The binary name is never silently tried
instead. -
check_affected and check_sbom registry matches carry
fixed_in(#2834; core-backend): the fixed
bound of the interval that matched the version asked about. For example, log4j-core 2.14.1 →
2.15.0 and 2.11.0 → 2.12.2. It isnullwith afixed_in_reasonwhen that interval has no fix
(it ends at last_affected, or has no upper bound). CPE matches carry nofixed_in.
Every cut level of check_affected's text keepsfixed_in, so a registry list near the cap of
200 no longer fits whole: on django's rows at 200 matches beside 50 undetermined, the text keeps
185 (29,863 characters) and the note says the last 15 are instructuredContent.dataonly. Its
description said every match stays in the text; it now says that of CPE matches, and that a
registry list near the cap can leave its last matches out. -
The server instructions and every tool description now fit in 2,048 characters (#2842). Claude
Code cuts each MCP tool description and each server's instructions at 2,048 characters
(CLIENT_DESCRIPTION_CUT, the strictest limit any client documents). On this release's text before the cut, 13 of 14
descriptions and the instructions (2,521) were over it, so a model in Claude Code read them cut.
exposure_radarwas 11,807 characters. Detail moved into the outputSchema description of the
field it describes. A test fails if any text grows past the limit, or if a required sentence
(the Shodan attribution, the hosted-endpoint disclosure, each tool's load-bearing rule) falls
after it. -
exposure_radar relays an observation window for kev_exposure (#2438; core-backend): the
Shodan banner times of the oldest and newest service×CVE pairs it counts, as
kev_exposure.window. That is passed through only when every counted pair has such a time.
Until then the note says how many have none. core-backend stores Shodan's bannertimestampas
banner_seen_at, and never our own write time; existing rows get one when a search matches
them again.statestays not_assessed andmeasured_atstays null: a window is a span of
observations made at different times, not one observation time. -
New prompt
workload_triage(#2846). It ranks an agent's own pods, images or CI containers by
CISA-KEV status, caller-asserted exposure and EPSS. The agent lists its workloads and makes an
SBOM per image digest with its own tools, then sends check_sbom only purls: no workload, image,
registry or exposure label goes to EchelonGraph. Exposure is labelled caller-asserted, and
unknown is never treated as not exposed. check_sbom's description says its input for container
images is an SBOM of each ...
echelongraph-mcp 2.6.8
Issue numbers (#NNNN) refer to EchelonGraph's internal issue tracker, which is not public: they are not issues of the public repository, and cannot be followed from it.
- get_cve and search_cves now say what
patch_availablemeans, and describepatch_evidence
(#2861). Through 2.6.7 neither tool describedpatch_available, and core-backend set it only
from Ubuntu and Red Hat patch records, so afalseread as "no patch": measured in production on
2026-10-05, 101,725 CVEs readfalsewhile EchelonGraph held evidence of a fix for them, 48,145
with a packagefixed_versionand 71,705 with an NVD reference tagged "Patch" (CVE-2017-5651,
Apache Tomcat, among them, with two package rows carrying afixed_version). core-backend now
setspatch_availabletrue whenever it holds evidence of a fix, and servespatch_evidence, the
list of its sources, strongest first:vendor_patch(a vendor patch on record, from Ubuntu or
Red Hat),fixed_version(a package fix version, or the fixed bound of an affected version
range) andnvd_patch_reference(an NVD reference tagged Patch). Both descriptions, and the
outputSchema descriptions of both fields, now state:patch_availableis true when EchelonGraph
holds evidence of a fix;falsemeans no fix evidence is on record, which is not a finding that
no fix exists; once true it is never set back to false, so it can be true beside an empty
patch_evidence; and an answer with nopatch_evidence(an API older than that field) does not
name the evidence. Both fields are relayed as the API sends them; apatch_evidencelabel the API
adds later is relayed, not a failed call. - search_cves: a page cut to fit the text keeps
patch_evidencebesidepatch_availablein each
row at the first cut level.
echelongraph-mcp 2.6.7
Issue numbers (#NNNN) refer to EchelonGraph's internal issue tracker, which is not public: they are not issues of the public repository, and cannot be followed from it.
- Tool descriptions now state facts only: no reporting instructions to the model and no references
to other tools, for the Anthropic MCP Directory policy ("Tool descriptions contain no
instructions about model behavior, other tools, or external instruction sources, and no hidden
or encoded text"). Tool behavior, results and their notes are unchanged. Reworded:- get_cve, search_cves: "The note labels each such CVE NOT YET SCORED: report it that way, never
as a score of 0." now reads "The note labels each such CVE NOT YET SCORED, which is not a score
of 0." - cve_summary: summary.rejected's "report them as withdrawn records, never as vulnerabilities"
now reads "they are withdrawn records, none of them a vulnerability"; summary.none's "report
those CVEs as not yet scored, not as CVEs rated None" now reads "and names those CVEs not yet
scored, not CVEs rated None". - get_vendor_advisory: "(the vendor and vendor_advisory_id fields of a row from
search_vendor_advisories or vendor_advisories_for_cve)" now reads "(the vendor and
vendor_advisory_id of a vendor advisory row)". - exposure_radar: "and cve_exposure says per CVE whether the radar tracks it" now reads "and this
answer does not say whether the radar tracks that CVE: tracking is answered per CVE". - epss_history: "Never interpolate it into a daily series." now reads "A daily series
interpolated from it holds values EchelonGraph never recorded, so the series is never a daily
series." - check_affected: "Read assessed before count: … a count of 0 there must never be reported as
not affected" now reads "count depends on assessed: … a count of 0 there is not a finding of
not affected"; the outputSchema description of assessed, "Read first.", now reads "count
depends on it.". - vendor_advisories_for_cve, get_vendor_advisory, search_vendor_advisories: the outputSchema
description of withdrawn, "report it as withdrawn", now reads "so it is a withdrawn advisory,
not a current one".
- get_cve, search_cves: "The note labels each such CVE NOT YET SCORED: report it that way, never
- test/description-policy.test.mjs lists the tools over stdio and fails if any title, description
or input/output schema description names another tool, matches a model-directed imperative
pattern, or carries zero-width, bidi or control characters or a base64-looking run.
echelongraph-mcp 2.6.6
Issue numbers (#NNNN) refer to EchelonGraph's internal issue tracker, which is not public: they are not issues of the public repository, and cannot be followed from it.
- Correction to 2.6.5 (#2775). Its entry says: "On the hosted endpoint it is the client going away
(its own timeout, a closed tab): every API call of that request is cancelled". That was not true
of the hosted endpoint. Cloud Run does not pass a client's disconnect on to a container it
speaks HTTP/1.1 to ("When you use HTTP/1.1 on Cloud Run, client disconnect events are not
propagated to the Cloud Run container",
https://docs.cloud.google.com/run/docs/troubleshooting). In production on 2026-10-05, four
check_sbomcalls had their client abort after 0.4 s or 1.5 s. Each was loggedstatus200
withclient_closedfalse and ran to its end, 2.0 to 5.3 s. Together they held both large-body
slots, so a large body sent 1 s later was answered 503. The same page says HTTP/2 does pass the
disconnect on. Sodist/http.jsnow serves HTTP/2 cleartext (h2c) whenMCP_H2C=1, and the
hosted deployment sets it together with Cloud Run's--use-http2. With that flag, Cloud Run
speaks only h2c to the container. The server acts on a reset stream the moment it arrives, with
any error code includingNO_ERROR: the API calls are cancelled,check_sbomsends no further
batch and ends aRetry-Afterwait, the large-body slot is freed, and themcp_requestline
saysstatus499 withclient_closedtrue. What Cloud Run forwards is only partly measured.
On test deployments with--use-http2on 2026-10-05, ordinary calls, sessions and/health
were all answered over h2c. A client that stopped reading a streamed answer had its stream reset
by Cloud Run 15 s in (rst_code2), and that was logged 499. A client that aborts mid-call
before any answer was not observed, because every probe call was answered first, so whether
Cloud Run forwards that reset at once is not yet shown. Until it is, an abandoned call may still
run to its end, acheck_sbomcall for at most its 50-second budget, holding its slot. A new line,mcp_client_gone,
records when the server learned the client went:after_msfrom the request's start, whether
the answer had started (answer_started), and an HTTP/2 stream'srst_code. It holds nothing
of the request. Run yourself,dist/http.jsspeaks HTTP/1.1 as before. WithMCP_H2C=1it
speaks h2c with prior knowledge only, and an HTTP/1.1 request gets no answer. - README, "Privacy: what is sent where": the hosted endpoint's paragraph now also says what the API logs
of the calls it makes for you (#2797): each under your address, with its URL path (for a CVE, CWE or
advisory lookup, the ID), and not its query string, the header-carried terms or which purls. The
privacy policy's new Section 11, https://echelongraph.io/privacy#hosted-mcp-endpoint , says the same. exposure_radarrelays and labelsmcp_servers.withheld_opted_out(#2822): how many hostnames
whose owner opted out of scanning the API left out of every othermcp_serversnumber. Such a
hostname is not checked again while the opt-out stands, so its last verdict cannot be re-checked;
the API now withholds it from the MCP-server counts as its unparameterised AI-exposure answer
already did. An answer without the field, from an API that predates it, counted those hostnames,
and the note says so instead of reading the absence as 0; a value that is not a whole number is
left out and named.- Claude Desktop in one click (#2815). From this release on, every GitHub release of
github.com/echelongraph/echelongraph-mcp
carriesechelongraph-mcp.mcpb, a Claude Desktop extension: open it and choose Install. It needs
no Node.js on your machine, since Claude Desktop runs it with the Node.js it ships, and no JSON to edit.
The README's Claude Desktop section links it as "Install in Claude Desktop", ahead of the custom
connector and thenpxsetup. The release workflow builds it from the release tag, the same tag
the npm package is published from:listings/mcpb/build.shvalidates the manifest with the pinned
MCPB CLI and packs. Then, on a separate read-only runner that never ran the MCPB CLI,
listings/mcpb/check-bundle.mjsrefuses a bundle that holds a top-level filebuild.shdoes not
stage, a symlink, or a file wherebuild.shstages a directory; that lacks the icon or entry point
its manifest names, ornode_modules/; whosepackage.json,LICENSE,README.mdordist/is not the tested npm
tarball's, byte for byte (a file changed, missing or added); whosenode_modules/adds or changes
a file a production install from the lockfile does not have; that is at another version; or whose
own server, started withPATH,HOMEandECHELONGRAPH_API_BASEonly (none of the caller's
NODE_PATH,NODE_OPTIONSor other variables), does not list the
manifest's 14 tools with their titles. npm publishes only after that check passes, so a manifest
or bundle defect stops the release before npm has the version. A separate job, which runs no
dependency code, signs the checked bundle with a SLSA build-provenance attestation (Sigstore,
keyless), attaches it and its Sigstore bundle to the release, each on its own, and checks the
file the release serves withgh attestation verify. To check a download yourself:
gh attestation verify echelongraph-mcp.mcpb --repo echelongraph/echelongraph-mcp.
Releases up to 2.6.5 carry no bundle. - Repository only, not in the package:
listings/mcpb/check-bundle.mjsand
test/mcpb-release.test.mjs;test/mcp-stdio-client.mjstakes acwd. sbom_reviewreads the advisory interval incheck_sbom'smatch_reasonby its closing
bracket, without the hedge of 2.6.5 and earlier.[A, B)is a fixed bound: B, the first version the advisory
records as not affected, is given as the fixed version ([A, ∞)names none).[A, B] (B is the last affected version, not a fix)is an OSVlast_affectedbound: B is still affected and the
range records no fix, so neither B nor any version at or below it is given as a fixed version.
The bracket alone decides: a cut text clipsmatch_reasonto 200 characters, which can drop the
words after it for a long package name, and where it is clipped before the closing bracket
([A, B…), no fixed version is taken from thatmatch_reason.
Through 2.6.5 core-backend rendered both bounds as[A, B), so the prompt gave B only as "the
end of that interval"; core-backend now renders alast_affectedbound closed (#2830). This
wording assumes the API serves the new rendering: it is released after core-backend is deployed.cve_intelrelaysfixed_brancheson eachaffected_packagesrow: every affected range of the
package on record, each withintroducedandfixed(the range's fix) or, where no fix is on
record,last_affected(never in a field named fixed), and withadvisory_id(the OSV record that
published the range) andsource(the loader's label for that record), from core-backend's
/api/v1/public/cves/:id/enrichmentsince #2817. Ranges that name their record are in the order
the record lists them; ranges loaded before the record was stored (both null) are oldest first.
fixed_branchesis null where a package's ranges were never loaded, and[]where no version
range is on record (none in the advisory, only commit ranges, or more than are stored), which is
not a finding that no fix exists.fixed_versionis one range's fix per package,
kept for compatibility: for CVE-2021-44228 production served log4j-core 2.12.2 alone (2026-10-04),
the fix for 2.4 to 2.12.1, older than an affected 2.14.1, while the advisory
(GHSA-jfh8-c2jp-5v3q) gives three ranges, fixed in 2.3.1, 2.12.2 and 2.15.0. The description says
how to pick the range for an installed version, and the note counts the rows with more than one
range, and those with no range on record (fixed_branchesnull, or[]beside afixed_version),
wherefixed_versionis all the answer holds. An API older than #2817 is said to be one (#2817).triage_cve's patch line gives each affected range with its fix fromfixed_branches, and
fixed_versiononly for a package without them;sbom_reviewgives a component the fix of the
fixed_branchesrange that holds its installed version (log4j-core 2.14.1: 2.15.0), after the
advisory interval incheck_sbom'smatch_reason, andfixed_versiononly for a package without
fixed_branches(#2817).vendor_advisories_for_cve,search_vendor_advisoriesandget_vendor_advisoryrelay each
vendor'sheld_since, which the coverage API now serves: the start of the window EchelonGraph has
read the vendor's advisories from, before which an advisory may not be held.
vendors_not_fully_heldreads it, not the earliest held advisory. For a vendor with no history read
the earliest held advisory is not where its window begins: 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. Cisco's earliest held
advisory is dated 2024-10-23 and Red Hat's 2009-02-04, so CVE-2025-20188 left out Cisco and
CVE-2014-0160 left out Red Hat, and their empty answers read as the vendor having published nothing;
both are listed now. GitHub is now listed too for any CVE ID year that begins before the start of
its first poll's 7-day window (held_since), not only for years up to that of its earliest held
advisory (2017). Against an API withoutheld_since, a vendor is taken as held from its earliest
advisory only when its history read is complete, and otherwise listed, its window written "not known
to be without a gap" (#2803).
echelongraph-mcp 2.6.5
Issue numbers (#NNNN) refer to EchelonGraph's internal issue tracker, which is not public: they are not issues of the public repository, and cannot be followed from it.
2.6.4 was tagged and never published: its Release run failed test/http-memory.test.mjs on Node 24,
where --max-old-space-size=128 alone allowed a 320 MiB heap in a 256 MiB instance. 2.6.5 is 2.6.4
plus the fix below; the v2.6.4 tag stays, unpublished, because a tag is never moved.
-
Hosted endpoint only:
Dockerfile.httpalso passes--max-semi-space-size=16, so V8's heap limit is
176 MiB on Node 22 (unchanged: the production image) and on Node 24 (was 320 MiB). The memory cap of
#2773 now holds whichever Node the image uses. -
README: the header links the directories that list this server, Smithery and Glama (#2723). Smithery's
verification looks for that link;test/listings.test.mjspins it. -
Hosted endpoint only: an instance serves at most 64 requests at once (
MCP_MAX_IN_FLIGHT); the
next, its body read first (so a slow upload holds no place), waits up to 10 seconds
(MCP_ADMISSION_WAIT_MS) for one to finish, and is then answered HTTP 503 withRetry-Afterand
JSON-RPC error-32030. A 2026-07-28subscriptions/listenstream is not counted: it stays open
until its client leaves, carrying only keepalives (the server announces no list changes), so 64
idle ones would leave every other request waiting 10 s for a 503; the SDK refuses a stream past
1,024 open per process. The image runs Node with V8's heap capped at 128 MiB, half the
instance's memory. Measured in a 256 MiB cgroup on a 16 GB host: two maximum-size SBOM documents
beside 125 or more other requests got the server killed with every request on it, and holding
that took both bounds. Node sized V8's heap from the host's memory, so garbage piled up until
the kernel killed the process (whether Cloud Run's instance reports more than its 256 MiB is not
verified), and 250 requests live at once exhausted even the capped heap. With both, 250 sent as
they come are all served (the last after waiting about 9 s), peaking at 82-84% of the memory,
and 250 at once are 64 served and 186 answered 503 after 10 s, peaking at 75-77%. Themcp_requestline gainsin_flightand
queued_ms, the start-up lineheap_limit_mbandmemory_limit_mb, and a process started
without the cap logsmcp_remote_heap_unbounded, where Node can read the container's limit
(memory_limit_mbnot null) (#2773). -
check_sbomstops when its call is cancelled: no further batch is sent, the batch in flight is
cut off, and aRetry-Afterwait ends. Over stdio that is the client's
notifications/cancelled, which before left the call sending its batches to the end. On the
hosted endpoint it is the client going away (its own timeout, a closed tab): every API call of
that request is cancelled, acheck_sbomcall holds its large-body slot until it has stopped,
not only until its client has gone, and themcp_requestline saysstatus499 with
client_closedtrue. Before, an abandoned call ran its batches to the end, spending the
client's API budget, and was logged as 200 (#2775). -
Repository only, not in the package: the production synthetic's hosted leg sends
check_sbom
its 201 purls as a CycloneDX document, a ~138,000-byte request body, twice the endpoint's
64 KiB general cap, and fails the probe unless the answer ismeasuredfrom the document; a
return of the 413 that #2747 removed now fails it (rpc_error,-32600). The synthetic's alert
runbook says how to read that failure, and a memory alert on the hosted endpoint is written
beside its uptime check (#2774, #2773). -
The prompts ask only for what their tools' first text block keeps, at their largest input. In
2.6.3 each of the four asked for something that text leaves out of a large answer, or that the
call it made did not return, so a client that passes onlycontentto the model never showed
it (#2799):kev_weekly_briefreadskev_recent50 rows a page, not 200. Each line of the brief names
kev_vuln_nameandkev_due_date. Read through the hosted endpoint on 2026-10-04 (2.6.3),
365 days at limit 200 (306 CVEs) kept 5 fields a row in the text, neither of those two among
them. A page of 50 comes back whole: 27,514 characters for production's 50 newest.check_affectedkeepsransomwareandepss_scoreon every match at every cut level, the
two fieldsam_i_affectedlists for each match. In 2.6.3 a list at the API's cap of 200
matches keptcve_id,kev_listed,effective_score,effective_severityand
score_assessed: hosted,linux_kernel5.10.0 had 20 CISA-KEV-listed matches and 1 with
known ransomware use, named nowhere in the text. Since 200 such matches with
effective_severityas well come to 33,240 characters, past the budget on their own, the
leanest level leaveseffective_severityout. Theexcludedandundeterminedsamples now
keep their first 10 entries in the text (excluded_countandundetermined_countcount every
entry), so every match still stays in the text. The description's "at least" list says so.sbom_reviewsends the purls not sent again fromdata.not_sent_purlsonly where the text
holds that list whole. Where the note says the text cuts it (to its first 10), the model
rebuilds the purls from the document instead: its distinct purls from the position the note
gives, in the order the note gives.check_sbom's note now gives that position, and the order
it counts the input's purls in (a CycloneDX document's components depth first, each one's
nested components right after it, each purl at its first place), whenever purls are not sent,
not only when the text cuts the list. The prompt also says what to do when the note says rows
are left out of the text: list the rows the text carries, and give the rest as
data.summary's counts.sbom_reviewreads each affected CVE's fixed versions fromcve_intel, called beside
get_cve(fixed_versionof itsaffected_packagesandfixed_versionsrows for the
component's package). In 2.6.3 it asked for "fixed versions where a result gives them", and
check_sbom's text keepsmatch_reason, the advisory interval that names the fixed version,
only at its first cut level: the 50 components of a Juice Shop SBOM are cut past it.sbom_reviewnever gives a fixed version that is not strictly greater than the component's
installed version, compared in the ecosystem's version order (where the model is not sure of
the order, it says so and gives none). It takes the end of the advisory interval in
match_reasonfirst, where the text keeps it, and otherwisecve_intel'sfixed_version,
said to be one value per package, the advisory's last range's, which need not be the fix on
the component's branch (#2817). Where no version qualifies, the line says the results give no
fixed version for the component's branch and points to the CVE's advisory (get_cve's
references). Read through the hosted endpoint on 2026-10-04 (2.6.3),cve_intelgives
log4j-core 2.12.2 for CVE-2021-44228, andcheck_sbom's matches for 2.14.1 say it "is listed
verbatim in the advisory's affected versions", so the entry above alone had the model give
2.12.2 as the fix for 2.14.1. The prompt also reads each component's ecosystem, package and version
from its purl, which every row the text keeps carries; the leanest cut, reached at 2,000 purls,
leaves the row's own fields out.
It maps a purl ascheck_sbom's matcher does: the sixteen purl types with an ecosystem (hex,
pub,swift,hackage,cran,bitnami,conanandgithubactamong them, which the
first wording left out),swiftandgithubactnamed with their namespace asnpm,golang
andcomposerare, anddeb,apk,alpineandrpmby adistroqualifier of
debian-N(its major release),ubuntu-Voralpine-X.Y(cut to major.minor); for any other
the ecosystem is unknown, and the fix list gives no version for it.triage_cve's patch line says thatcve_intel'sfixed_versionis one value per package, the
advisory's last range's, and is given as whatcve_intelrecords for that package, never as
the fix for every affected branch; for a branch's fix it points toget_cve's references
tagged Vendor Advisory or Patch and to the vendor advisories (#2817). The line had asked for
"fixed versions" alone, with no installed version to hold them against, so for CVE-2021-44228
it could give log4j-core 2.12.2,cve_intel's one value on 2026-10-04, as the patch, below the
affected 2.14.1.triage_cvereads CISA's due date,kev_due_date, fromget_cve's record and no longer calls
kev_recent. Its step 6 read it from akev_recentpage of the CVE'skev_added_date, with no
limit and no paging. Read through the hosted endpoint on 2026-10-04 (2.6.3), the page for
2021-11-03 held 50 of the 287 CVEs CISA added that day, and not CVE-2021-40444, one of them;
get_cvefor CVE-2021-40444 carried itskev_due_datein its first text block.
-
cve://{cve_id}stays under the 60,000-character ceiling. Itsdatais cut asget_cve's
first text block is, laid out the same way, after the envelope. In 2.6.3 it wasget_cve's
structured result pretty-printed whole: 86,559 characters for CVE-2021-44228 on the hosted
endpoint, against 23,705 now on the suite's production-shaped answer. Its notes no longer carry
get_cve'sTEXT CUTsentences, which describe a first text block the resource does not have
(they saidcpe_matchkept 64 entries while the resource carried all 396). Whendatais cut,
the notes end with the resource's own sentence instead, startingDATA CUT, which names
get_cve's `structuredCon...
echelongraph-mcp 2.6.3
exposure_radarrelays the observation window each count was measured over, where the API now
serves one:exposed_databases.window(when our verifier last confirmed each host),
leaked_credentials.window(when each key was last seen) andshadow_ai.confirmed_exposed.window
(when our verifier's deciding probe ran). A window is relayed only when no counted row is undated;
otherwise the note says how many are, and a malformed window is left out and named. KEV-exposure
carries no observation time yet.exposure_radarstaysnot_assessedwithmeasured_atnull:
a window's end is not one observation time (#2438).vendor_advisories_for_cve,search_vendor_advisoriesandget_vendor_advisoryread each
vendor's window fromGET /api/v1/public/vendor-advisories/coveragebeside their own request,
and relay it incoverage(vendor_windows, orvendor_windowfor the detail): the advisories
held,earliest_vendor_published_at,latest_vendor_published_atandhistory_backfill.
vendor_advisories_for_cveaddscve_yearandvendors_not_fully_held, the vendors with no
advisory in the answer of which EchelonGraph holds none, whose earliest held advisory is dated
after 1 January of the year in the CVE ID, or whose history is still being read, and its note
names each with its window: an empty answer for CVE-2024-3400 can no longer be read as Palo Alto
having published none. Its description no longer says an empty answer means none of the polled
feeds names the CVE: it means none of the advisories EchelonGraph holds does. The search
and the detail name the vendors whose history is still being read. When the windows cannot be
read, the answer is still relayed, measured, with the windows null and a note saying so (#2729).search_vendor_advisories: a query of 1 or 2 characters matches whole words only
(search_matchword), as the API now answers it, and the description and the note say so; a
search's total is counted to at most 1,000, and a capped total (total_cappedtrue) is written
"1,000+" in the note, never as a count.coveragegainstotal_cappedandsearch_match
(#2728).check_sbom's 50-second budget bounds the whole call, not only when a batch may start: each
batch's request may take only what is left of it, and a batch still unanswered when the budget
runs out is cut off and its purls answered as not sent (not_sent_reasontime_budget). Before,
a batch started at 49.9 s could run for the 15-second request timeout, past the 60-second limits
of the SDK's client and the hosted endpoint, which then answered nothing at all. A Retry-After
wait that ends at the budget's end, before any batch was answered, answers that 429 instead of
sending a request with no time left (#2756).- Repository only, not in the package: the production synthetic probes
check_sbomwith 201
purls, so every run sends two batches, and fails the probe (expectation_unmet) unless both
were answered; core-backend's tests fail whenMAX_COMPONENTS,MAX_PURL_LENor
API_COMPONENTS_PER_MINUTEdisagree with the API's own values, and every core-backend deploy runs
them before it builds (scripts/check-outbound-identity.sh's table), refusing on drift (#2757). - A success's first text block is cut to fit 30,000 characters when the API's JSON is longer, and
structuredContent.datastill carries the answer whole. Through 2.6.2 that block was the JSON
whatever its size: on production's answers of 2026-10-04 a defaultsearch_cvesfor "openssl"
was 133,300 characters of text,get_cveCVE-2021-44228 80,897,check_affectedopenssl 3.0.0
156,058 (the Linux kernel at 5.10.0, 364,408),check_sbomon a 50-component SBOM 112,820,
kev_recentat limit 200 105,925 andsearch_vendor_advisoriesat limit 50 103,147, against
the 60,000-character ceiling; the same calls now measure 13,513, 22,960, 25,951 (27,717),
28,733, 31,588 and 19,917. The JSON is first laid out one row a line, which loses nothing; past
that, a list tool cuts each row to the fields it names (the description cut to 200 characters,
every long string ending in "…"), then to fewer, and leaves rows out only when its leanest cut
is still too long (check_sbomleaves outnot_affectedrows first);get_cvekeeps the first
entries ofcpe_matchandreferences. The note then ends with a sentence starting
TEXT CUTthat names what the first text block leaves out, which tool returns a row whole, and,
when rows are left out, theoffsetor page size at which they fit. An answer that fits is sent
as before. Every tool description, the server instructions and the methodology resource say so
(#2783). search_cvestakesoffset(0 to 10,000), so the next page a cut text names can be read (#2783).- A list beside the rows is cut before the rows are:
check_affected'sexcludedand
undeterminedsamples keep each entry'scve_idandreason, andcve_ids(each match's
cve_id, in order) its first 10, so every match stays in the text (flash_player10.0.0, 200
matches and 50 excluded entries, had 75 matches left out of it, two of them KEV-listed), andcheck_sbom'snot_sent_purlskeeps its first 10, with the note
saying from which position of the input the purls not sent run. A 2,000-purlcheck_sbomcall
that the API's budget stops after 1,200 now keeps the affected rows before the clean ones in the
text. A cutcheck_affectedmatch keepsmatch_reason(a registry match's advisory interval,
which names the fixed version) where it fits, and the note namescve_intelfor fixed versions
(#2783).
echelongraph-mcp 2.6.2
kev_recentreads an RFC 3339sinceoruntilas the date written in it, dropping the time
and offset. It used to convert the timestamp to UTC first, so2024-04-12T00:00:00+02:00was
answered for 2024-04-11, and2024-04-12T23:30:00-05:00for 2024-04-13, while still reporting
a measured result.kev_added_dateis a calendar date, so the written date is the one meant.
Ztimestamps, such as thoseget_cvereturns, are unaffected (#2755).- Hosted endpoint only: a
tools/callofcheck_sbom, or aprompts/getofsbom_review, may
now send a request body of up to 6 MiB, so a real CycloneDX or SPDX document (up to the tool's
5,000,000-character cap) is accepted overhttps://mcp.echelongraph.io/mcp; before, any body
over 64 KiB was refused, and only a purl list fit. Every other request keeps the 64 KiB cap.
Each server instance serves at most two such large bodies at once; a third is answered HTTP 503
withRetry-Afterand JSON-RPC error-32030. The per-client limit is unchanged (120 requests
a minute). New settingsMCP_MAX_SBOM_BODY_BYTESandMCP_LARGE_BODY_SLOTS. Thecheck_sbom
description and the README say that over the hosted endpoint the document reaches
EchelonGraph's server, which reads the purls from it and neither logs nor keeps it (#2747). cve_summarysays what the answer'spollerblock is: the in-memory counters of the NVD
poller of the one API instance that answered, since it last started, zeroed on every restart,
never the feed's size, intake, reliability or freshness. The note says so whenever the block is
present, quotingcves_ingested,poll_countandpoll_errors; the description and the
outputSchema describe the block and each counter. The JSON is still relayed as sent (#2647).
Issue numbers (#NNNN) refer to EchelonGraph's internal issue tracker, which is not public: they are not issues of this repository, and cannot be followed from it.
Install: npx -y echelongraph-mcp@2.6.2, or connect to the hosted endpoint https://mcp.echelongraph.io/mcp (no install, no key).
Provenance: published by this repository's release workflow from tag v2.6.2 with an npm SLSA provenance attestation. Verify with npm audit signatures.