Skip to content

Releases: echelongraph/echelongraph-mcp

echelongraph-mcp 3.0.0

Choose a tag to compare

@github-actions github-actions released this 11 Oct 17:52

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_in is 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 answered fixed_in 4.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_detail names the ranges it moved past. The API applies the rule; the server relays
    it. cve_intel carries no fixed_in: its description, its fixed_branches schema description
    and its note now state the same walk for fixed_branches (an affected version's fix is the
    first fixed above it that no range of its row holds, none past a range without one), where
    its description had said the fixed of 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_remediation carries fix_status, the API's fix-status rule that CVE Pulse pages show
    (available, fix_referenced, fix_merged, none_confirmed or unknown, with its basis),
    and get_cve's remediation summary 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 is available with basis cna_solution and solution_by (the sentence, statement,
    and the CNA that wrote it, cna); a fix patch_evidence counts from the record with no sentence
    to quote is available with basis patch_evidence_statement and stated_in_record; and
    unread_inputs can name cna_record.

  • patch_evidence has 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
    CSAF vendor_fix, Microsoft's Vendor Fix), cna_solution (a sentence of the CVE record's
    solutions that names a fixed version) and description_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_source cna), which is not the
    record's own published; and cna (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_04 where 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_decision and score_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 its kev_required_action description, and cve_remediation's note and
    description.

  • cve_remediation takes the installed version: ecosystem, package and version, all three
    or none, sent in the X-EG-Ecosystem, X-EG-Package and X-EG-Version request headers and
    never in the URL. It then answers installed (that version's verdict against the CVE),
    fixed_in and, when it is null, fixed_in_reason; that answer is private and no-store.
    Fix tags on references match in either case (#2841, #3073).

  • The sbom_review prompt reads a fixed version from fixed_in alone, and names the source
    package where a row matched upstream (matched_via upstream, 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.md and
    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: true for its data whole.
    Claude Code hands the model structuredContent, not the text
    blocks, and refuses one past MAX_MCP_OUTPUT_TOKENS (25,000 by default), saving it to a file:
    get_cve for CVE-2021-44228 was 65,540 characters of structuredContent on production beside a
    first text block of 23,759, so the model got a file path, not an answer. Through 2.7.x
    structuredContent.data was 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 to index, purl,
    verdict, not_assessed_reason, cve_ids, matched_package and matched_via, each match to
    cve_id, kev_listed, ransomware, epss_score, effective_score, score_assessed,
    fixed_in and fixed_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's product_ids, then
    without its text, the note then naming get_vendor_advisory as where each text is;
    search_cves' rows to their description and scores, then the scores; the vendor-advisory tools' rows
    without affected_products, then without summary. Then lists leave entries: get_cve's
    cpe_match and references first; search_cves', kev_recent's, get_cwe's and the vendor-advisory
    tools' rows from the end; check_affected's excluded and undetermined samples, then its
    matches, cve_ids last; check_sbom's and scan_manifest's not_sent_purls and not_checked, then
    their results, the not_affected rows leaving first, then the not_assessed ones, and only then
    affected rows, from the end; cve_remediation's vendor_remediations, then patches;
    exposure_radar's daily and weekly series, each keeping its newest entries, then its rankings,
    then kev_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's not_checked beside 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 carries data_truncated (false when data is whole), and, when it
    is true, truncated: each list cut, by its path in data, with total, returned and, where its
    entries are thinned, thinned: true; the note ends with sentences starting DATA CUT that 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
    with full: true returns data whole. Every tool takes full
    (boolean, optional). A program that relied on data whole must pass full: 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 in structuredContent.data with full: 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_reason beside
    fixed_in at every cut level that keeps matches. Their note, and the sbom_review and
    workload_triage prompts, say data.not_sent_purls lists 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 of data.not_checked (#3013).

  • The triage_cve and sbom_review prompts read a vendor's workaround or mitigation step from
    get_vendor_advisory where 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...

Read more

echelongraph-mcp 2.7.2

Choose a tag to compare

@github-actions github-actions released this 11 Oct 08:24

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), and eg_first_seen_at, when EchelonGraph recorded
    it. first_seen_at is kept, deprecated, as source_published_at under its old key (#2876).

echelongraph-mcp 2.7.1

Choose a tag to compare

@github-actions github-actions released this 06 Oct 11:40

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 to prompts/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_error log lines now carry error_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

Choose a tag to compare

@github-actions github-actions released this 06 Oct 01:17

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. Pass files (1 to 20 of filename and
    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-After waits
    and cancellation as check_sbom (that loop now lives in src/tools/match_batch.ts, shared by both
    tools, check_sbom's behaviour unchanged). Read: requirements.txt (== and === pins only),
    go.mod (require with replace and exclude applied), 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 in data.not_checked with its file, line and reason
    (version_unpinned, version_unresolved, local_path, vcs_source, unsupported_line) and is
    not clean: django>=4 is never sent as pkg: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's fixed_in is 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/call of scan_manifest up to 6 MiB, as it does check_sbom
    (LARGE_BODY_TARGETS now 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_listed and epss_score on 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),
    and vendor_advisories_for_cve for 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_ids on 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 gets cve_rejected
    for the CVE asked for. The vendor's own withdrawn is unchanged: it still says only whether the
    vendor rescinded its advisory. The note opens with CVE REJECTED for 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
    keeps rejected_cve_ids at both cut levels.

  • GitHub advisories whose text is a CVE rejection notice ("Rejected reason: …") are now served
    with withdrawn: 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_description and kev_notes_urls
    (the https URLs in CISA's notes, at most 10), alongside kev_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_evidence has 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 references patch in
      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 tagged patch. 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_date on 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 for libssl3 and 262 for openssl. Through 2.6.8,
    pkg:deb/debian/libssl3@…?upstream=openssl and pkg:apk/alpine/libcrypto3@… came back
    not_assessed, while the same version under openssl was affected. Each row now says
    matched_package and matched_via (purl_name, upstream or coordinates), and a match found
    through upstream= says so in its match_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 is null with a fixed_in_reason when that interval has no fix
    (it ends at last_affected, or has no upper bound). CPE matches carry no fixed_in.
    Every cut level of check_affected's text keeps fixed_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 in structuredContent.data only. 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_radar was 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 banner timestamp as
    banner_seen_at, and never our own write time; existing rows get one when a search matches
    them again. state stays not_assessed and measured_at stays 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 ...

Read more

echelongraph-mcp 2.6.8

Choose a tag to compare

@github-actions github-actions released this 05 Oct 14:44

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_available means, and describe patch_evidence
    (#2861). Through 2.6.7 neither tool described patch_available, and core-backend set it only
    from Ubuntu and Red Hat patch records, so a false read as "no patch": measured in production on
    2026-10-05, 101,725 CVEs read false while EchelonGraph held evidence of a fix for them, 48,145
    with a package fixed_version and 71,705 with an NVD reference tagged "Patch" (CVE-2017-5651,
    Apache Tomcat, among them, with two package rows carrying a fixed_version). core-backend now
    sets patch_available true whenever it holds evidence of a fix, and serves patch_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) and nvd_patch_reference (an NVD reference tagged Patch). Both descriptions, and the
    outputSchema descriptions of both fields, now state: patch_available is true when EchelonGraph
    holds evidence of a fix; false means 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 no patch_evidence (an API older than that field) does not
    name the evidence. Both fields are relayed as the API sends them; a patch_evidence label the API
    adds later is relayed, not a failed call.
  • search_cves: a page cut to fit the text keeps patch_evidence beside patch_available in each
    row at the first cut level.

echelongraph-mcp 2.6.7

Choose a tag to compare

@github-actions github-actions released this 05 Oct 11:44

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".
  • 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

Choose a tag to compare

@github-actions github-actions released this 05 Oct 08:28

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_sbom calls had their client abort after 0.4 s or 1.5 s. Each was logged status 200
    with client_closed false 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. So dist/http.js now serves HTTP/2 cleartext (h2c) when MCP_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 including NO_ERROR: the API calls are cancelled, check_sbom sends no further
    batch and ends a Retry-After wait, the large-body slot is freed, and the mcp_request line
    says status 499 with client_closed true. What Cloud Run forwards is only partly measured.
    On test deployments with --use-http2 on 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_code 2), 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, a check_sbom call 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_ms from the request's start, whether
    the answer had started (answer_started), and an HTTP/2 stream's rst_code. It holds nothing
    of the request. Run yourself, dist/http.js speaks HTTP/1.1 as before. With MCP_H2C=1 it
    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_radar relays and labels mcp_servers.withheld_opted_out (#2822): how many hostnames
    whose owner opted out of scanning the API left out of every other mcp_servers number. 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
    carries echelongraph-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 the npx setup. The release workflow builds it from the release tag, the same tag
    the npm package is published from: listings/mcpb/build.sh validates 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.mjs refuses a bundle that holds a top-level file build.sh does not
    stage, a symlink, or a file where build.sh stages a directory; that lacks the icon or entry point
    its manifest names, or node_modules/; whose package.json, LICENSE, README.md or dist/ is not the tested npm
    tarball's, byte for byte (a file changed, missing or added); whose node_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 with PATH, HOME and ECHELONGRAPH_API_BASE only (none of the caller's
    NODE_PATH, NODE_OPTIONS or 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 with gh 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.mjs and
    test/mcpb-release.test.mjs; test/mcp-stdio-client.mjs takes a cwd.
  • sbom_review reads the advisory interval in check_sbom's match_reason by 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 OSV last_affected bound: 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 clips match_reason to 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 that match_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 a last_affected bound closed (#2830). This
    wording assumes the API serves the new rendering: it is released after core-backend is deployed.
  • cve_intel relays fixed_branches on each affected_packages row: every affected range of the
    package on record, each with introduced and fixed (the range's fix) or, where no fix is on
    record, last_affected (never in a field named fixed), and with advisory_id (the OSV record that
    published the range) and source (the loader's label for that record), from core-backend's
    /api/v1/public/cves/:id/enrichment since #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_branches is 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_version is 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_branches null, or [] beside a fixed_version),
    where fixed_version is 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 from fixed_branches, and
    fixed_version only for a package without them; sbom_review gives a component the fix of the
    fixed_branches range that holds its installed version (log4j-core 2.14.1: 2.15.0), after the
    advisory interval in check_sbom's match_reason, and fixed_version only for a package without
    fixed_branches (#2817).
  • vendor_advisories_for_cve, search_vendor_advisories and get_vendor_advisory relay each
    vendor's held_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_held reads 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 without held_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

Choose a tag to compare

@AkshayDubey29 AkshayDubey29 released this 05 Oct 02:19

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.http also 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.mjs pins 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 with Retry-After and
    JSON-RPC error -32030. A 2026-07-28 subscriptions/listen stream 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%. The mcp_request line gains in_flight and
    queued_ms, the start-up line heap_limit_mb and memory_limit_mb, and a process started
    without the cap logs mcp_remote_heap_unbounded, where Node can read the container's limit
    (memory_limit_mb not null) (#2773).

  • check_sbom stops when its call is cancelled: no further batch is sent, the batch in flight is
    cut off, and a Retry-After wait 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, a check_sbom call holds its large-body slot until it has stopped,
    not only until its client has gone, and the mcp_request line says status 499 with
    client_closed true. 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 is measured from 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 only content to the model never showed
    it (#2799):

    • kev_weekly_brief reads kev_recent 50 rows a page, not 200. Each line of the brief names
      kev_vuln_name and kev_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_affected keeps ransomware and epss_score on every match at every cut level, the
      two fields am_i_affected lists for each match. In 2.6.3 a list at the API's cap of 200
      matches kept cve_id, kev_listed, effective_score, effective_severity and
      score_assessed: hosted, linux_kernel 5.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_severity as well come to 33,240 characters, past the budget on their own, the
      leanest level leaves effective_severity out. The excluded and undetermined samples now
      keep their first 10 entries in the text (excluded_count and undetermined_count count every
      entry), so every match still stays in the text. The description's "at least" list says so.
    • sbom_review sends the purls not sent again from data.not_sent_purls only 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_review reads each affected CVE's fixed versions from cve_intel, called beside
      get_cve (fixed_version of its affected_packages and fixed_versions rows for the
      component's package). In 2.6.3 it asked for "fixed versions where a result gives them", and
      check_sbom's text keeps match_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_review never 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_reason first, where the text keeps it, and otherwise cve_intel's fixed_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_intel gives
      log4j-core 2.12.2 for CVE-2021-44228, and check_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 as check_sbom's matcher does: the sixteen purl types with an ecosystem (hex,
      pub, swift, hackage, cran, bitnami, conan and githubact among them, which the
      first wording left out), swift and githubact named with their namespace as npm, golang
      and composer are, and deb, apk, alpine and rpm by a distro qualifier of
      debian-N (its major release), ubuntu-V or alpine-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 that cve_intel's fixed_version is one value per package, the
      advisory's last range's, and is given as what cve_intel records for that package, never as
      the fix for every affected branch; for a branch's fix it points to get_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_cve reads CISA's due date, kev_due_date, from get_cve's record and no longer calls
      kev_recent. Its step 6 read it from a kev_recent page of the CVE's kev_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_cve for CVE-2021-40444 carried its kev_due_date in its first text block.
  • cve://{cve_id} stays under the 60,000-character ceiling. Its data is cut as get_cve's
    first text block is, laid out the same way, after the envelope. In 2.6.3 it was get_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's TEXT CUT sentences, which describe a first text block the resource does not have
    (they said cpe_match kept 64 entries while the resource carried all 396). When data is cut,
    the notes end with the resource's own sentence instead, starting DATA CUT, which names
    get_cve's `structuredCon...

Read more

echelongraph-mcp 2.6.3

Choose a tag to compare

@AkshayDubey29 AkshayDubey29 released this 04 Oct 11:37
  • exposure_radar relays 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) and shadow_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_radar stays not_assessed with measured_at null:
    a window's end is not one observation time (#2438).
  • vendor_advisories_for_cve, search_vendor_advisories and get_vendor_advisory read each
    vendor's window from GET /api/v1/public/vendor-advisories/coverage beside their own request,
    and relay it in coverage (vendor_windows, or vendor_window for the detail): the advisories
    held, earliest_vendor_published_at, latest_vendor_published_at and history_backfill.
    vendor_advisories_for_cve adds cve_year and vendors_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_match word), 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_capped true) is written
    "1,000+" in the note, never as a count. coverage gains total_capped and search_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_reason time_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_sbom with 201
    purls, so every run sends two batches, and fails the probe (expectation_unmet) unless both
    were answered; core-backend's tests fail when MAX_COMPONENTS, MAX_PURL_LEN or
    API_COMPONENTS_PER_MINUTE disagree 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.data still 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 default search_cves for "openssl"
    was 133,300 characters of text, get_cve CVE-2021-44228 80,897, check_affected openssl 3.0.0
    156,058 (the Linux kernel at 5.10.0, 364,408), check_sbom on a 50-component SBOM 112,820,
    kev_recent at limit 200 105,925 and search_vendor_advisories at 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_sbom leaves out not_affected rows first); get_cve keeps the first
    entries of cpe_match and references. The note then ends with a sentence starting
    TEXT CUT that names what the first text block leaves out, which tool returns a row whole, and,
    when rows are left out, the offset or 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_cves takes offset (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's excluded and
    undetermined samples keep each entry's cve_id and reason, and cve_ids (each match's
    cve_id, in order) its first 10, so every match stays in the text (flash_player 10.0.0, 200
    matches and 50 excluded entries, had 75 matches left out of it, two of them KEV-listed), and check_sbom's not_sent_purls keeps its first 10, with the note
    saying from which position of the input the purls not sent run. A 2,000-purl check_sbom call
    that the API's budget stops after 1,200 now keeps the affected rows before the clean ones in the
    text. A cut check_affected match keeps match_reason (a registry match's advisory interval,
    which names the fixed version) where it fits, and the note names cve_intel for fixed versions
    (#2783).

echelongraph-mcp 2.6.2

Choose a tag to compare

@AkshayDubey29 AkshayDubey29 released this 04 Oct 07:37
  • kev_recent reads an RFC 3339 since or until as the date written in it, dropping the time
    and offset. It used to convert the timestamp to UTC first, so 2024-04-12T00:00:00+02:00 was
    answered for 2024-04-11, and 2024-04-12T23:30:00-05:00 for 2024-04-13, while still reporting
    a measured result. kev_added_date is a calendar date, so the written date is the one meant.
    Z timestamps, such as those get_cve returns, are unaffected (#2755).
  • Hosted endpoint only: a tools/call of check_sbom, or a prompts/get of sbom_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 over https://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
    with Retry-After and JSON-RPC error -32030. The per-client limit is unchanged (120 requests
    a minute). New settings MCP_MAX_SBOM_BODY_BYTES and MCP_LARGE_BODY_SLOTS. The check_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_summary says what the answer's poller block 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, quoting cves_ingested, poll_count and poll_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.