echelongraph-mcp 3.1.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/README.md +2 -2
- package/dist/http.js +4 -3
- package/dist/httpPolicy.js +9 -4
- package/dist/index.js +38 -5
- package/dist/inputBounds.js +38 -0
- package/dist/prompts.js +2 -2
- package/dist/schemaRefusal.js +10 -1
- package/dist/tools/check_sbom.js +32 -3
- package/dist/tools/scan_manifest.js +2 -0
- package/docs/rate-limits.md +3 -2
- package/docs/structured-results.md +8 -0
- package/package.json +3 -3
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,79 @@ with a provenance attestation by the release workflow of
|
|
|
6
6
|
[github.com/echelongraph/echelongraph-mcp](https://github.com/echelongraph/echelongraph-mcp),
|
|
7
7
|
from the commit tagged `v<version>`.
|
|
8
8
|
|
|
9
|
+
## 3.1.1 — 2026-10-25
|
|
10
|
+
|
|
11
|
+
A security release (#3345). It moves the runtime dependencies past three advisories, bounds what
|
|
12
|
+
a tool call's arguments may hold, and takes npm out of the hosted endpoint's image. The hosted
|
|
13
|
+
endpoint's 3.1.0 image was refused by its deploy-time Trivy scan (#3018: 13 HIGH with a fixed
|
|
14
|
+
version), so the endpoint kept serving 3.0.2. No `outputSchema` changes, so a client holding
|
|
15
|
+
3.1.0's tool list does not need to reconnect; only the three `inputSchema` bounds below are new.
|
|
16
|
+
|
|
17
|
+
### Security
|
|
18
|
+
|
|
19
|
+
- **@modelcontextprotocol/server 2.1.0 to 2.3.0** (CVE-2026-111768, CVSS 7.5; CVE-2026-111770,
|
|
20
|
+
CVSS 7.7), with @modelcontextprotocol/core 2.3.0. Both stay exact pins, as does zod below.
|
|
21
|
+
- CVE-2026-111770 (a server or transport shared between connections delivered one client's
|
|
22
|
+
messages to another): **not affected**, on either transport. The hosted endpoint builds a new
|
|
23
|
+
server for each request (`createMcpHandler` with `createServer` as its factory), and stdio one
|
|
24
|
+
per connection (`serveStdio`), which the advisory names as not affected. 2.3.0 refuses a shared
|
|
25
|
+
server outright (`ALREADY_CONNECTED`); a test holds that 30 clients one after another and 30 at
|
|
26
|
+
once, in both protocol eras, over the hosted endpoint and over stdio, are each answered with
|
|
27
|
+
their own record.
|
|
28
|
+
- CVE-2026-111768 (validating an unbounded array or object in a tool call's arguments exhausts
|
|
29
|
+
memory): **mitigated, now fixed**. Through 3.1.0 only the hosted endpoint's request-body cap
|
|
30
|
+
(64 KiB, or 6 MiB for `check_sbom`, `scan_manifest` and `sbom_review`), the advisory's
|
|
31
|
+
stopgap, bounded it, and nothing did on stdio. The server now sets the SDK's new
|
|
32
|
+
`maxToolInputElements` to 2,500,000 list entries and object members per call, counted at every
|
|
33
|
+
depth: the most any tool accepts, a `check_sbom` document passed as an object at its
|
|
34
|
+
5,000,000-character cap, so no call a tool would answer is refused by it. A call over it is
|
|
35
|
+
refused by the SDK before any tool runs, with `isError` true and one text block of the SDK's
|
|
36
|
+
own ("Invalid arguments for tool <name>: arguments contain more than the maximum of 2500000
|
|
37
|
+
elements"), and without `structuredContent`: the SDK offers no way to answer it in the
|
|
38
|
+
envelope (docs/structured-results.md).
|
|
39
|
+
- A third advisory 2.3.0 fixes, GHSA-rvq5-wwqv-78pq (bearer authentication accepted a token
|
|
40
|
+
issued for another service), does not apply: this server uses no authentication.
|
|
41
|
+
- **zod 4.6.5 to 4.6.6** (CVE-2023-54404, GHSA-mrhx-c7xc-m3w5, CVSS 7.5). 4.6.6 checks a list's
|
|
42
|
+
bound before it parses an element of it, but only where the list has a bound. Every list and
|
|
43
|
+
record in a tool's `inputSchema` now has one, published in `tools/list` as `maxItems` or
|
|
44
|
+
`maxProperties` and checked before any element is read: `check_sbom`'s `purls` at most 10,000
|
|
45
|
+
entries, its `sbom` passed as an object at most 64 top-level members
|
|
46
|
+
(CycloneDX 1.6 defines 18 and SPDX 2.3 16), and `scan_manifest`'s `files` at most 20 (the
|
|
47
|
+
README's 1 to 20). A call past one is refused with `state` `invalid_input` and a note naming the
|
|
48
|
+
bound: `purls must hold at most 10000 entries`, `sbom must hold at most 64 members`, `files must
|
|
49
|
+
hold at most 20 entries` (through 3.1.0, "21 files; at most 20 are read per call"). The
|
|
50
|
+
`purls` bound counts entries, repeats included, not distinct purls. The README's 2,000 is the
|
|
51
|
+
distinct purls a call checks: that rule is unchanged and still `check_sbom`'s own (more than
|
|
52
|
+
2,000 distinct, in a list or a document, is refused with the same message as through 3.1.0). A
|
|
53
|
+
list may repeat a purl, as a flattened SBOM does once per install path, so the entry bound is
|
|
54
|
+
five times the distinct one. Of `purls` lists, one kind 3.1.0 accepted is newly refused: a list
|
|
55
|
+
of more than 10,000 entries, which 3.1.0 checked when it held at most 2,000 distinct purls, each
|
|
56
|
+
sent once. That is inherent in bounding a list that had no bound. A list of up to 10,000
|
|
57
|
+
entries is answered as through 3.1.0. Over the hosted endpoint a `check_sbom` call may be a 6 MiB body,
|
|
58
|
+
which 10,000 entries of even the longest purl the API takes (512 characters) fit.
|
|
59
|
+
- **Hosted endpoint only: its image carries no npm** (#3018). The node base bundles npm, npx and
|
|
60
|
+
corepack, which this server never runs, and its npm carried 10 of the 13 HIGH the scan refused
|
|
61
|
+
3.1.0's image for: brace-expansion 2.0.2 (5), ip-address 10.1.0, pacote 19.0.2 and 20.0.1,
|
|
62
|
+
picomatch 4.0.3 and sigstore 3.1.0; the other 3 were the SDK's two and zod's. The image's
|
|
63
|
+
runtime stage (Dockerfile.http) now removes npm, npx and corepack, and its build fails if any is
|
|
64
|
+
left; its base digest is unchanged. Trivy v0.75.0 (the scan's pinned scanner) finds the 13 on a
|
|
65
|
+
copy of 3.1.0's runtime file system and none on 3.1.1's (2026-10-25). The npm package and the
|
|
66
|
+
stdio image (Dockerfile, for the Docker MCP Catalog) are unchanged by this.
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
|
|
70
|
+
- **Hosted endpoint only: a 2026-07-28 `subscriptions/listen` stream ends right after its
|
|
71
|
+
acknowledgement**, with a `complete` result. SDK 2.2.0 ends a listen stream that honours none of
|
|
72
|
+
the notification types asked for, and this server sends none (it declares no `listChanged`).
|
|
73
|
+
Through 3.1.0 the stream stayed open, carrying only keepalives, until the client left. A listen
|
|
74
|
+
still takes none of the 64 places an instance serves at once. Over stdio a listen is unchanged.
|
|
75
|
+
- The SDK's packages declare the Apache-2.0 license since 2.3.0 (their LICENSE file carries the
|
|
76
|
+
MIT terms of earlier contributions too); echelongraph-mcp stays MIT.
|
|
77
|
+
- **workload_triage's step 4 sends each purl once** (#3345): it has the agent pass several images'
|
|
78
|
+
purls in one `check_sbom` call, and images share packages, so such a list can repeat purls. The
|
|
79
|
+
step now says to send each purl once, and that a list holds at most 10,000 entries, the new
|
|
80
|
+
`purls` bound above.
|
|
81
|
+
|
|
9
82
|
## 3.1.0 — 2026-10-25
|
|
10
83
|
|
|
11
84
|
- **cve_summary's `freshness` is the fleet's last completed NVD poll** (#3276). Since #3242 the
|
package/README.md
CHANGED
|
@@ -252,7 +252,7 @@ The example questions are ones a client can answer with that tool alone.
|
|
|
252
252
|
| `kev_recent` | Recent CISA KEV additions | The CVEs CISA has added to its Known Exploited Vulnerabilities catalog, newest first (`kev_added_date`), from EchelonGraph's copy of the catalog, polled from CISA every 5 minutes: due date, vendor, product, known ransomware use, EchelonGraph's severity, CVSS, EPSS and `eg_kev_tier`, and `our_first_seen_kev`. Filter by date range, ransomware and vendor; page with `limit` and `next_cursor`. Dated by `last_successful_fetch_at`, our last successful fetch of CISA's feed; the filters travel as request headers, never in the URL. | *Which CVEs has CISA added to KEV since 1 September, and which have known ransomware use?* |
|
|
253
253
|
| `epss_history` | EPSS change history for one CVE | How one CVE's EPSS score has changed, as EchelonGraph recorded it: one point per recorded change (`series_kind` `change_only`), never a daily series, with the value now; `complete_since`, from when every change is a point (before it the record misses changes, so a missing point there does not mean unchanged, and the series is never called complete); `coverage`, which FIRST score dates the record holds and which it misses (`days_missing`, `days_partial`, `days_in_progress`); and `series_starts_at`, when recording began; before it a missing point means not recorded, not unchanged. | *How has CVE-2023-44487's EPSS score changed, as EchelonGraph recorded it?* |
|
|
254
254
|
| `check_affected` | Am I affected? (product or package at a version) | Whether a product (its NVD CPE product token) or a registry package (`ecosystem` and `package`) at a given version is affected by known CVEs, from the matcher behind echelongraph.io/am-i-affected: `assessed` first (false: not evaluated, with `not_assessed_reason`, and a count of 0 then is not "not affected"), the matching CVEs with `kev_listed`, `ransomware`, `epss_score`, `effective_score` and `score_assessed` (a registry match also `fixed_in`, the lowest version above it that no affected interval of the record holds, with `fixed_in_detail` when it moved past an overlapping interval, or null with `fixed_in_reason`, which is not a finding that no fix exists), and advisories it cannot decide counted as `undetermined_count`, never as safe. What you look up travels in request headers, never in the URL. | *Is openssl 3.0.0 affected by known CVEs? Is lodash 4.17.15 on npm?* |
|
|
255
|
-
| `check_sbom` | Check an SBOM against the advisory corpus | A dependency list checked against EchelonGraph's advisory corpus (OSV.dev records), one verdict per component: `affected`, `not_affected`, `undetermined` or `not_assessed`, each with its `not_assessed_reason`. Each row names the package its advisories were looked up by (`matched_package`) and how (`matched_via`: `upstream` where a deb, apk or alpine purl's upstream qualifier names the source package, which is how Debian and Alpine file advisories, else `purl_name`), and each match carries `fixed_in`, the lowest version above the component's that no affected interval of the record holds (`fixed_in_detail` names any overlapping interval it moved past), or null with `fixed_in_reason`. Pass up to 2,000 distinct purls, or a CycloneDX JSON or SPDX JSON document: the purls are read from it by the MCP server and only they are sent to the API, in POST bodies of at most 200 each, one after another; the document itself is not sent on. Run from npm, the server is on your machine; over the hosted endpoint (`mcp.echelongraph.io`) it is EchelonGraph's, and the document is the request body.
|
|
255
|
+
| `check_sbom` | Check an SBOM against the advisory corpus | A dependency list checked against EchelonGraph's advisory corpus (OSV.dev records), one verdict per component: `affected`, `not_affected`, `undetermined` or `not_assessed`, each with its `not_assessed_reason`. Each row names the package its advisories were looked up by (`matched_package`) and how (`matched_via`: `upstream` where a deb, apk or alpine purl's upstream qualifier names the source package, which is how Debian and Alpine file advisories, else `purl_name`), and each match carries `fixed_in`, the lowest version above the component's that no affected interval of the record holds (`fixed_in_detail` names any overlapping interval it moved past), or null with `fixed_in_reason`. Pass up to 2,000 distinct purls, in a list of at most 10,000 entries, or a CycloneDX JSON or SPDX JSON document: the purls are read from it by the MCP server and only they are sent to the API, in POST bodies of at most 200 each, one after another; the document itself is not sent on. Run from npm, the server is on your machine; over the hosted endpoint (`mcp.echelongraph.io`) it is EchelonGraph's, and the document is the request body. At the API's per-component rate limit (429), the tool waits out its `Retry-After`, within 50 seconds a call; past that it answers what it has, with the purls not sent counted in `coverage.not_sent`, the reason in `coverage.not_sent_reason`, and the list in `data.not_sent_purls` (whole with `full: true`; a `DATA CUT` can cut it), never dropped silently. A deb, apk or rpm purl without a `distro` qualifier naming its release is not assessed (`distro_release_unknown`): EchelonGraph does not guess one. Only `not_affected` is clean. No ranking, no score. | *Check this CycloneDX SBOM against the advisory corpus.* |
|
|
256
256
|
| `scan_manifest` | Check a lockfile against the advisory corpus | A project's lockfiles or pinned manifests checked against the same advisory corpus, one verdict per dependency, through the same batch route, batches, 50-second budget and `Retry-After` waits as `check_sbom`. Pass `files`, 1 to 20 of `filename` and `content` (the file's text), up to 5,000,000 characters in all; the format is read from the filename, or set by `format`. Read: `requirements.txt` (only `==` and `===` pins; extras and markers stripped), `go.mod` (`require`, single-line and block, `// indirect` included, with `replace` and `exclude` applied), `package-lock.json` and `npm-shrinkwrap.json` (lockfileVersion 1 to 3), `Cargo.lock`, `Gemfile.lock` (platform suffixes stripped), `composer.lock` (a leading `v` stripped), `poetry.lock` and `gradle.lockfile`. `package.json`, `pyproject.toml`, `Pipfile`, `Gemfile`, `Cargo.toml`, `composer.json` and `build.gradle` hold ranges and are refused, each naming the lockfile to pass; `go.sum` is refused, as it lists module versions the build does not select; `pom.xml`, `yarn.lock` and `pnpm-lock.yaml` are not read yet. The MCP server reads the files into purls and only the purls are sent to the API; filenames and file contents are not sent on. Run from npm, the server is on your machine; over the hosted endpoint (`mcp.echelongraph.io`) it is EchelonGraph's, and the files are the request body. `data.results` holds one row per distinct purl, as `check_sbom`'s do, each match with `kev_listed`, `epss_score` and `fixed_in`. Every entry not sent is in `data.not_checked`, with its file, line and `reason`: `version_unpinned` (a range, such as `django>=4`, whose bound is never sent as a version), `version_unresolved`, `local_path`, `vcs_source` or `unsupported_line`, counted in `coverage.not_checked_by_reason`; they are not clean. A file that holds no version this tool can check answers `not_assessed` without calling the API. Only `not_affected` is clean. No ranking, no score. | *Check this package-lock.json against the advisory corpus.* |
|
|
257
257
|
| `cve_intel` | CVE weakness, exploits and packages | Weakness, exploit code and detection checks (a Nuclei template, or a GitHub repository the API serves as a checker, checks whether a system is affected; it is not necessarily exploit code, and the note counts them apart), affected packages and fixed versions for one CVE, from EchelonGraph's per-CVE enrichment: `cwes`, `exploits` (at most 10, verified first) with `exploits_total`, `exploits_capped`, `exploits_by_kind` and `exploits_by_status`, `affected_packages`, `fixed_versions` and `timeline`. Each `affected_packages` row carries `fixed_branches`, every affected range of the package with its fix (`introduced`, and `fixed` or, where no fix is on record, `last_affected`), each naming the OSV record that published it (`advisory_id`, `source`) and in that record's order: an installed version's fix is the first `fixed` above it that no range of its row holds (the `fixed` of the range that holds it, moved past any range that still holds that version; none past a range without `fixed`), compared in the ecosystem's version order. `fixed_version` is one range's fix, kept for compatibility, not the fix for every range; `fixed_branches` is null where a package's ranges were never loaded, and `[]` where no version range is on record, which is not a finding that no fix exists. `verified_status` is the label stored with each reference, not a guarantee that the exploit works. An empty `exploits` list is not evidence that no public exploit exists; a section the API could not read is named in `coverage.sections_failed`, never relayed as an empty list. | *Is there public exploit code for CVE-2021-44228, and which versions fix it?* |
|
|
258
258
|
| `cve_remediation` | How one CVE is fixed | How one CVE is fixed, as its sources state it, in one answer; every text is relayed as its source states it, untested by EchelonGraph: `cisa` (for a KEV-listed CVE, CISA's `required_action`, `due_date`, `short_description` and `notes_urls`, verbatim; CISA directs the action at US federal civilian agencies, and it is not EchelonGraph's advice), `fixed_branches` (each affected package with every affected range and its fix), `vendor_remediations` (each vendor advisory naming the CVE, at most 20, with its `remediation_state` and the items it lists for the CVE, each with `kind`, the vendor's own category: `vendor_fix`, `workaround`, `mitigation`, `no_fix_planned`, `none_available` or `other`), `fix_references` (references NVD or the CNA tagged Patch or Mitigation), `patches`, `coverage` and `failed_sections`, with `fix_status`, the API's fix-status rule that CVE Pulse pages show. Given the installed `ecosystem`, `package` and `version` (all three or none), it also answers `installed` (that version's verdict) and `fixed_in`: the first version above it that no affected range on record holds, or null with `fixed_in_reason`. `remediation_state` `none_in_source` or `not_parsed`, and an empty list, are not a finding that no fix exists. A REJECTED CVE answers `not_assessed`. | *How is CVE-2021-44228 fixed, as its sources state it?* |
|
|
@@ -458,7 +458,7 @@ and `cve_intel`'s `fixed_branches` where the text keeps no match for a component
|
|
|
458
458
|
|
|
459
459
|
## Structured results
|
|
460
460
|
|
|
461
|
-
Since 2.0.0 every result, success or failure, carries `structuredContent`, and every tool
|
|
461
|
+
Since 2.0.0 every result a tool answers, success or failure, carries `structuredContent`, and every tool
|
|
462
462
|
declares its shape as an `outputSchema` in `tools/list`, with a title and the annotations
|
|
463
463
|
`readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true` and
|
|
464
464
|
`openWorldHint: true` (hints, which a client treats as untrusted).
|
package/dist/http.js
CHANGED
|
@@ -255,9 +255,10 @@ async function serveMcpTimed(req, res, pathname, timing) {
|
|
|
255
255
|
// is held to that.
|
|
256
256
|
if (body.length > MAX_BODY_BYTES && !isLargeBodyTarget(facts))
|
|
257
257
|
return { ...tooLarge(res, cors), body, facts };
|
|
258
|
-
// A 2026-era subscriptions/listen is a stream
|
|
259
|
-
//
|
|
260
|
-
//
|
|
258
|
+
// A 2026-era subscriptions/listen is a stream that does no work here (since SDK 2.2.0 it ends
|
|
259
|
+
// right after its acknowledgement; through 2.1.0 it stayed open until its client left): it
|
|
260
|
+
// takes no place and holds no work, or 64 idle ones would leave every other request waiting
|
|
261
|
+
// for a 503 (#2773; httpPolicy.ts isListenStream says what bounds it).
|
|
261
262
|
const standard = { protocolVersion: header(req, "mcp-protocol-version"), mcpMethod: header(req, "mcp-method"), mcpName: header(req, "mcp-name") };
|
|
262
263
|
if (isListenStream(facts, body, standard))
|
|
263
264
|
return { ...(await relay(req, res, pathname, method, cors, client, body, facts, gone.signal)), facts };
|
package/dist/httpPolicy.js
CHANGED
|
@@ -438,14 +438,19 @@ export class Admission {
|
|
|
438
438
|
// ── The request that is a stream: subscriptions/listen ──
|
|
439
439
|
//
|
|
440
440
|
// A 2026-07-28 client opens subscriptions/listen to hear of list changes, and the SDK answers it
|
|
441
|
-
// with an SSE stream
|
|
442
|
-
//
|
|
443
|
-
// the SDK's keepalives
|
|
441
|
+
// with an SSE stream. Through @modelcontextprotocol/server 2.1.0 (echelongraph-mcp 3.1.0) the
|
|
442
|
+
// stream stayed open until the client left (or the server closed), carrying the acknowledgement
|
|
443
|
+
// and then only the SDK's keepalives, since this server declares no listChanged (index.ts): no
|
|
444
|
+
// work, no API call. Since SDK 2.2.0 (its #2651; 3.1.1, #3345) a stream that honours none of the
|
|
445
|
+
// notification types asked for ends right after the acknowledgement, with a resultType "complete"
|
|
446
|
+
// result, so here every listen ends at once (test/http.test.mjs holds both). The rest of this
|
|
447
|
+
// comment is why a listen still takes no place: a later listChanged: true would hold it open again.
|
|
448
|
+
// Admitted like any other request, it held its place
|
|
444
449
|
// for as long as it was open, so 64 idle listeners (under the per-client limit) left every
|
|
445
450
|
// tools/call, initialize and server/discover on the instance waiting 10 s for a 503 (#2773). So it
|
|
446
451
|
// is relayed without admission and without a work hold. What bounds the streams instead: the
|
|
447
452
|
// per-client limit, which counts each one when it opens; the SDK's own cap of
|
|
448
|
-
// DEFAULT_MAX_SUBSCRIPTIONS (1024 in @modelcontextprotocol/server 2.1.0) open per process, past
|
|
453
|
+
// DEFAULT_MAX_SUBSCRIPTIONS (1024 in @modelcontextprotocol/server 2.1.0 and 2.3.0) open per process, past
|
|
449
454
|
// which it answers JSON-RPC -32603 "Subscription limit reached" at once; and on Cloud Run the
|
|
450
455
|
// instance's 250 requests at once and its 60 s request timeout, which ends every stream
|
|
451
456
|
// (deploy-all.sh SVC_CONCURRENCY, --timeout). Measured 2026-10-04 on dist/http.js, Node 22.22,
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
// unless the whole structured result would pass 30,000 characters of JSON: then, unless the call
|
|
35
35
|
// says full: true, its lists are cut in the order the tool names (DATA CUT, #3013), data_truncated
|
|
36
36
|
// is true and truncated gives each list cut (data_truncated is false otherwise). Each tool
|
|
37
|
-
// advertises the envelope as its outputSchema, which `data` validates against cut or whole.
|
|
37
|
+
// advertises the envelope as its outputSchema, which `data` validates against cut or whole. The
|
|
38
|
+
// one result without it is the SDK's own refusal of arguments over MAX_TOOL_INPUT_ELEMENTS
|
|
39
|
+
// (#3345), which no tool's handler sees.
|
|
38
40
|
//
|
|
39
41
|
// The envelope in the text (#2440, #2467). Many clients pass only `content` to the model, so the
|
|
40
42
|
// last text block of every result is structuredContent serialized as JSON, less what an earlier
|
|
@@ -61,8 +63,8 @@ import * as z from "zod";
|
|
|
61
63
|
import { registerKevRecent } from "./tools/kev_recent.js";
|
|
62
64
|
import { registerEPSSHistory } from "./tools/epss_history.js";
|
|
63
65
|
import { registerCheckAffected } from "./tools/check_affected.js";
|
|
64
|
-
import { registerCheckSbom } from "./tools/check_sbom.js";
|
|
65
|
-
import { registerScanManifest } from "./tools/scan_manifest.js";
|
|
66
|
+
import { MAX_PURL_ENTRIES, MAX_SBOM_CHARS, registerCheckSbom } from "./tools/check_sbom.js";
|
|
67
|
+
import { MAX_FILES, registerScanManifest } from "./tools/scan_manifest.js";
|
|
66
68
|
import { registerCveIntel } from "./tools/cve_intel.js";
|
|
67
69
|
import { registerCveRemediation } from "./tools/cve_remediation.js";
|
|
68
70
|
import { fixStatusSchema } from "./fixStatus.js";
|
|
@@ -3052,11 +3054,42 @@ const SUMMARY_REJECTED_DESCRIPTION = "summary.rejected counts the CVE records re
|
|
|
3052
3054
|
const SCORE_ASSESSED_DESCRIPTION = "echelongraph_score, echelongraph_severity and echelongraph_risk are EchelonGraph's score only when score_assessed is true. With score_assessed false the CVE is NOT YET SCORED, not scored 0: any of those three it carries (0, NONE, 0) is a placeholder, not a rating, and does not mean the CVE is harmless; score_unassessed_reason says why, and the note labels each such CVE NOT YET SCORED. An answer with no score_assessed (an API older than that field) does not say whether the CVE was scored, the note says so, and a 0 there is not a rating either.";
|
|
3053
3055
|
// The shared helpers a tool in src/tools/ is handed (#2718), so its file holds its own logic.
|
|
3054
3056
|
const TOOL_KIT = { api, succeeded, failed, badInput, crashed, checked, okHead, envelopeSchema, annotations: ANNOTATIONS, cveIdArg: CVE_ID_ARG, instant: Instant };
|
|
3057
|
+
// #3345 (CVE-2026-111768): the most array elements and object members, counted together at every
|
|
3058
|
+
// depth, that one tools/call's arguments may hold. The SDK (2.3.0, McpServer's
|
|
3059
|
+
// maxToolInputElements, off unless set) counts them before any inputSchema runs, and answers a
|
|
3060
|
+
// call over it with isError: true and one text block of its own, "Invalid arguments for tool
|
|
3061
|
+
// <name>: arguments contain more than the maximum of <n> elements", without structuredContent: its
|
|
3062
|
+
// tools/call handler offers no hook that would let badInput's envelope answer it
|
|
3063
|
+
// (test/input-elements.test.mjs holds that shape). Through 3.1.0 nothing bounded this on stdio,
|
|
3064
|
+
// and on the hosted endpoint only the 6 MiB body cap did (httpPolicy.ts), about 3,145,000 elements.
|
|
3065
|
+
//
|
|
3066
|
+
// The limit is the largest input a tool accepts, from each tool's own limits, so no call a tool
|
|
3067
|
+
// would answer is refused by it:
|
|
3068
|
+
// - check_sbom { sbom: <object>, full }: the document is at most MAX_SBOM_CHARS (5,000,000)
|
|
3069
|
+
// characters as JSON. There every element takes at least 2 characters (a one-character value
|
|
3070
|
+
// and its comma, as in [0,0,…]) and an object document 4 more ({"":[ … ]}), so it holds at
|
|
3071
|
+
// most (5,000,000 - 4) / 2 = 2,499,998 elements; with sbom and full, 2,500,000. One more is a
|
|
3072
|
+
// document over 5,000,000 characters, which check_sbom refuses anyway.
|
|
3073
|
+
// - check_sbom { purls: […], full }: MAX_PURL_ENTRIES (10,000) entries, the most its inputSchema
|
|
3074
|
+
// takes, repeats included (it checks at most MAX_PURLS, 2,000, distinct): 10,002.
|
|
3075
|
+
// - scan_manifest { files: […], full }: MAX_FILES (20) files of filename, content and format:
|
|
3076
|
+
// 2 + 20 x 4 = 82. A lockfile's lines and entries are inside its content, one string.
|
|
3077
|
+
// - every other tool: at most 7 arguments, none of them a list or an object.
|
|
3078
|
+
// A purl list, a set of files or an SBOM's top level far smaller than this is refused by its own
|
|
3079
|
+
// inputSchema bound first (#3345), before zod reads an element of it.
|
|
3080
|
+
const SBOM_OBJECT_MAX_ELEMENTS = Math.floor((MAX_SBOM_CHARS - 4) / 2);
|
|
3081
|
+
export const MAX_TOOL_INPUT_ELEMENTS = Math.max(2 + SBOM_OBJECT_MAX_ELEMENTS, 2 + MAX_PURL_ENTRIES, 2 + MAX_FILES * 4);
|
|
3055
3082
|
// One server, built per connection by serveStdio for whichever era the client opens with, and
|
|
3056
|
-
// per request by the hosted HTTP entrypoint (http.ts, #2316): one tool module for both.
|
|
3083
|
+
// per request by the hosted HTTP entrypoint (http.ts, #2316): one tool module for both. A server
|
|
3084
|
+
// serves one connection (SDK 2.3.0's fix for CVE-2026-111770), which this per-connection and
|
|
3085
|
+
// per-request factory never shares (test/per-request-server.test.mjs).
|
|
3057
3086
|
// tools/list answers in registration order, which is the order below.
|
|
3058
3087
|
export function createServer() {
|
|
3059
|
-
const server = new McpServer({ name: NAME, version: VERSION }, {
|
|
3088
|
+
const server = new McpServer({ name: NAME, version: VERSION }, {
|
|
3089
|
+
instructions: INSTRUCTIONS,
|
|
3090
|
+
capabilities: { tools: { listChanged: false }, prompts: { listChanged: false }, resources: { listChanged: false } },
|
|
3091
|
+
maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS,
|
|
3092
|
+
});
|
|
3060
3093
|
// #3015: arguments that break a tool's inputSchema get badInput's envelope (state invalid_input,
|
|
3061
3094
|
// notes naming each argument and its rule), not the SDK's bare "Input validation error" text.
|
|
3062
3095
|
// Before the first registerTool, so every tool below and in src/tools/ is covered; tools/list's
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// #3345 (CVE-2023-54404, GHSA-mrhx-c7xc-m3w5): every list and every record in a tool's
|
|
2
|
+
// inputSchema has an upper bound, checked before any element of it is parsed.
|
|
3
|
+
//
|
|
4
|
+
// zod through 4.6.5 parsed every element of a container before checking its size, so a huge list
|
|
5
|
+
// or object cost its full parse whatever its bound. 4.6.6 checks a leading .max(), .length() or
|
|
6
|
+
// .size() of a z.array, z.set or z.map first, and refuses an oversized one before parsing any
|
|
7
|
+
// element (core/schemas.ts capSize; only the checks before any other check, so .max() comes first
|
|
8
|
+
// in each chain). A container without a bound is still parsed whole, and z.record has no size
|
|
9
|
+
// check at all. So each list here is .max()'d first, and a record is boundedRecord(): an identity
|
|
10
|
+
// transform whose check counts the object's own members and refuses past `max` before the record
|
|
11
|
+
// parses a key or value, piped into the record, the way zod's own fromJSONSchema enforces
|
|
12
|
+
// maxProperties (classic/from-json-schema.ts checkObjectGuards). tools/list publishes maxItems and
|
|
13
|
+
// maxProperties; test/input-bounds.test.mjs walks every tool's inputSchema and fails on a list or
|
|
14
|
+
// record without one.
|
|
15
|
+
//
|
|
16
|
+
// The SDK's element limit (index.ts MAX_TOOL_INPUT_ELEMENTS, CVE-2026-111768) bounds a call's
|
|
17
|
+
// arguments as a whole; these bound each container to what its tool documents.
|
|
18
|
+
import * as z from "zod";
|
|
19
|
+
/**
|
|
20
|
+
* A z.record(z.string(), value) of at most `max` own members, refused past `max` before any key or
|
|
21
|
+
* value is parsed (as a too_big issue of origin "object"), and published with maxProperties.
|
|
22
|
+
*/
|
|
23
|
+
export function boundedRecord(value, max) {
|
|
24
|
+
const guard = z.transform((v) => v).check((payload) => {
|
|
25
|
+
const v = payload.value;
|
|
26
|
+
if (typeof v !== "object" || v === null || Array.isArray(v))
|
|
27
|
+
return;
|
|
28
|
+
// Counted up to max + 1 only: an oversized object costs no more than that.
|
|
29
|
+
let n = 0;
|
|
30
|
+
for (const k in v)
|
|
31
|
+
if (Object.prototype.hasOwnProperty.call(v, k) && ++n > max)
|
|
32
|
+
break;
|
|
33
|
+
if (n > max) {
|
|
34
|
+
payload.issues.push({ origin: "object", code: "too_big", maximum: max, inclusive: true, message: `Too big: expected object to have <=${max} properties`, input: v, continue: false });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return guard.pipe(z.record(z.string(), value)).meta({ maxProperties: max });
|
|
38
|
+
}
|
package/dist/prompts.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
-
import { MAX_COMPONENTS, MAX_PURLS, MAX_SBOM_CHARS, RECHECK_CALLS } from "./tools/check_sbom.js";
|
|
2
|
+
import { MAX_COMPONENTS, MAX_PURL_ENTRIES, MAX_PURLS, MAX_SBOM_CHARS, RECHECK_CALLS } from "./tools/check_sbom.js";
|
|
3
3
|
export const KEV_BRIEF_DEFAULT_DAYS = 7;
|
|
4
4
|
export const KEV_BRIEF_MAX_DAYS = 365;
|
|
5
5
|
// The page the brief reads kev_recent in (#2799). The brief puts kev_vuln_name and kev_due_date on
|
|
@@ -338,7 +338,7 @@ export function workloadTriageText(source) {
|
|
|
338
338
|
`1. List the workloads and the digest of each image they run, with your own tools. ${each(WORKLOAD_LIST)} An image you cannot resolve to a digest is listed with its tag as not pinned: a tag can later point at another image.`,
|
|
339
339
|
`2. Decide each workload's exposure yourself, from the same data, as exposed, not exposed or unknown, and label it caller-asserted. ${each(WORKLOAD_EXPOSURE)} Unknown is never treated as not exposed. EchelonGraph measured none of this: no EchelonGraph tool reports the exposure of your workloads.`,
|
|
340
340
|
"3. Make an SBOM of each distinct digest with your own tools, for example syft <image>@sha256:<digest> -o cyclonedx-json, or trivy image --format cyclonedx <image>@sha256:<digest>. Take the purls of its components, leave out the purl of the image itself (pkg:oci or pkg:docker), and keep your own map from each purl to the images that carry it.",
|
|
341
|
-
`4. Call check_sbom with purls set to one image's purls, or to several images' purls together, up to ${MAX_PURLS} distinct purls per call. Pass purls, not the SBOM document: a document's metadata can name the image. data.results holds one row per purl sent, each with its purl, which your map ties to its images. If coverage.not_sent is above 0, say how many purls were not sent and coverage.not_sent_reason, and that they are not checked (never clean); after a minute, call check_sbom once more with purls set to the purls not sent: data.not_sent_purls, or, where the note says the first text block cuts that list or a DATA CUT cuts it in data, the distinct purls you sent from the position the note gives on, in the order you sent them.`,
|
|
341
|
+
`4. Call check_sbom with purls set to one image's purls, or to several images' purls together, up to ${MAX_PURLS} distinct purls per call, each purl once (a list may hold at most ${MAX_PURL_ENTRIES} entries, repeats included). Pass purls, not the SBOM document: a document's metadata can name the image. data.results holds one row per purl sent, each with its purl, which your map ties to its images. If coverage.not_sent is above 0, say how many purls were not sent and coverage.not_sent_reason, and that they are not checked (never clean); after a minute, call check_sbom once more with purls set to the purls not sent: data.not_sent_purls, or, where the note says the first text block cuts that list or a DATA CUT cuts it in data, the distinct purls you sent from the position the note gives on, in the order you sent them.`,
|
|
342
342
|
`5. For each affected row, read kev_listed, ransomware, epss_score, effective_score, score_assessed and fixed_in from its matches where the text or data keeps them. A row whose matches a DATA CUT shortened or left out carries matches_truncated. Where matches_truncated has kev_listed_cve_ids and ransomware_cve_ids, they name every KEV-listed and every ransomware CVE among all of the row's matches, those cut included, so count each of them as a KEV-listed (or ransomware) CVE of the row; where it has total and returned and neither list (the leanest cut), nothing in the answer names the row's KEV-listed or ransomware CVEs, and the get_cve calls below read them. Call get_cve for each CVE of an affected row that the text or data keeps without its match (a row with cve_ids and no matches, or with fewer matches than cve_ids, as matches_truncated says), and for each match that leaves out one of the fields above, to read kev_listed, kev_ransomware, epss_score, score_assessed, echelongraph_score and cvss_v3_score; past ${WORKLOAD_GET_CVE_MAX} such CVEs, list the rest as not looked up, unranked.`,
|
|
343
343
|
`6. data.summary counts every row of a call whatever the cut: affected, not_affected, undetermined and not_assessed, and not_assessed_by_reason. Take each image's counts from it, and a workload's from its images': where the note says rows of results are left out of data (a DATA CUT sentence saying "results keeps K of its N entries") and the call carried several images' purls, call check_sbom with one image's purls per call for its counts. A row left out whose verdict is not_affected or not_assessed is not checked again: data.summary counts it, and it holds no CVE; where not_assessed rows are left out, state the image's not_assessed count and not_assessed_by_reason from data.summary instead of its OS-package rows, and say so. The affected and undetermined rows that left data are the ones checked again: the DATA CUT sentence names them by index, those with a KEV-listed or ransomware match first and apart ("affected or undetermined rows: the K with a KEV-listed or ransomware match at index …; the other K at index …"), each index the position, from 0, of a purl among the distinct purls you sent, in the order you sent them. Call check_sbom again with those purls alone as purls, in the order the note names them, so those with a KEV-listed or ransomware match first, in calls of the sizes its re-check sentence gives ("Re-check these K purls in this order, in calls of …"; a purl it sends to a call of its own goes alone), not of the page sentence's N, which holds for the first N purls you sent and not for these; read their rows as in step 5; each call's own note says what its data leaves out. If that takes more than ${RECHECK_CALLS} calls, stop after ${RECHECK_CALLS} and report the affected and undetermined rows not checked again, by purl where the note names them and by count, as unchecked, never as clean.`,
|
|
344
344
|
ENVELOPE_RULES,
|
package/dist/schemaRefusal.js
CHANGED
|
@@ -69,6 +69,9 @@ export function issueText(issue, input) {
|
|
|
69
69
|
return `${name} must be ${cmp} ${n} ${n === "1" ? "character" : "characters"} long`;
|
|
70
70
|
if (issue.origin === "array" || issue.origin === "set")
|
|
71
71
|
return `${name} must hold ${cmp} ${n} ${n === "1" ? "entry" : "entries"}`;
|
|
72
|
+
// #3345: a record's bound (inputBounds.ts boundedRecord), counted in its own members.
|
|
73
|
+
if (issue.origin === "object" || issue.origin === "record")
|
|
74
|
+
return `${name} must hold ${cmp} ${n} ${n === "1" ? "member" : "members"}`;
|
|
72
75
|
return `${name} must be ${cmp} ${n}`;
|
|
73
76
|
}
|
|
74
77
|
case "invalid_value":
|
|
@@ -82,8 +85,14 @@ export function issueText(issue, input) {
|
|
|
82
85
|
case "invalid_format":
|
|
83
86
|
return `${name} must be ${issue.format ? `a valid ${issue.format}` : "in the format the inputSchema gives"}`;
|
|
84
87
|
case "invalid_union": {
|
|
88
|
+
// #3345: the value is of the type one branch takes and broke that branch's rule (an sbom
|
|
89
|
+
// object past its bound): name that branch's issues, at the union's path.
|
|
90
|
+
const ofType = (b) => b.length === 1 && b[0].code === "invalid_type" && !b[0].path?.length;
|
|
91
|
+
const matched = (issue.errors ?? []).filter((b) => b.length > 0 && !ofType(b));
|
|
92
|
+
if (matched.length === 1)
|
|
93
|
+
return matched[0].map((i) => issueText({ ...i, path: [...(issue.path ?? []), ...(i.path ?? [])] }, input)).join("; ");
|
|
85
94
|
// Each branch refused only the value's type: name the types the branches take.
|
|
86
|
-
const types = (issue.errors ?? []).map((b) => (b
|
|
95
|
+
const types = (issue.errors ?? []).map((b) => (ofType(b) ? TYPE_NAMES[b[0].expected ?? ""] : undefined));
|
|
87
96
|
const named = types.length > 0 && types.every((t) => t !== undefined) ? [...new Set(types)] : undefined;
|
|
88
97
|
return named ? `${name} must be ${listOf(named)}, not ${jsonType(sent)}` : `${name} must take one of the forms the inputSchema allows, not ${jsonType(sent)}`;
|
|
89
98
|
}
|
package/dist/tools/check_sbom.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { answering, FULL_ARG, TEXT_BUDGET_DESCRIPTION } from "../textBudget.js";
|
|
3
|
+
import { boundedRecord } from "../inputBounds.js";
|
|
3
4
|
import { CLEAN_NOTE, DISTRO_NOTE, FRESHNESS_NOTE, MAX_PURLS, MEASURED_AT_NOTE, METHOD, affectedSaidOver, batchDataShape, dedupe, field, isObj, plural, purlsRefusal, sendBatches, sendingCoverage, sendingCoverageShape, sendingSentences, verdictSentences, } from "./match_batch.js";
|
|
4
5
|
// #2835: the batch loop, its constants and the row schema moved to match_batch.ts, which
|
|
5
6
|
// scan_manifest shares; re-exported here, where the tests and prompts.ts import them.
|
|
@@ -9,6 +10,27 @@ export const CHECK_SBOM = "check_sbom";
|
|
|
9
10
|
// components) is 0.74 MB; this leaves room for a much larger one while bounding what this process
|
|
10
11
|
// parses.
|
|
11
12
|
export const MAX_SBOM_CHARS = 5_000_000;
|
|
13
|
+
// #3345: the most top-level members an sbom passed as an object may have, checked before zod
|
|
14
|
+
// reads any of them (inputBounds.ts boundedRecord). CycloneDX 1.6 defines 18 ($schema, bomFormat,
|
|
15
|
+
// specVersion, serialNumber, version, metadata, components, services, externalReferences,
|
|
16
|
+
// dependencies, compositions, properties, vulnerabilities, annotations, formulation, declarations,
|
|
17
|
+
// definitions, signature), SPDX 2.3 16 and SPDX 3.0's JSON-LD 2; 64 leaves room for a later
|
|
18
|
+
// version's and a generator's own. What a member holds is bounded by the document's character cap
|
|
19
|
+
// above and by the arguments' element limit (index.ts MAX_TOOL_INPUT_ELEMENTS).
|
|
20
|
+
export const MAX_SBOM_MEMBERS = 64;
|
|
21
|
+
// #3345: the most entries purls may hold, repeats included, checked before zod reads any
|
|
22
|
+
// (inputBounds.ts). Not MAX_PURLS, the most DISTINCT purls a call checks, counted after trimming
|
|
23
|
+
// and dedupe (purlsRefusal): a list may repeat a purl, as a flattened SBOM does once per install
|
|
24
|
+
// path, and through 3.1.0 such a list was checked with each purl sent once. Five times MAX_PURLS,
|
|
25
|
+
// room for each of 2,000 distinct purls five times over; only a list past it, which 3.1.0 had no
|
|
26
|
+
// bound for, is newly refused. It is reached over the hosted endpoint as on stdio (no body cap
|
|
27
|
+
// there): a check_sbom call may be a large body (httpPolicy.ts, up to 6 MiB), and 10,000 entries
|
|
28
|
+
// of realistic purls (22 to 120 characters, npm to rpm with qualifiers) are a 0.25 to 1.2 MB
|
|
29
|
+
// tools/call body, of the longest the backend takes (MAX_PURL_LEN, 512) 5.15 MB
|
|
30
|
+
// (test/input-bounds.test.mjs holds that under the cap). Its 10,002 elements are far below the
|
|
31
|
+
// arguments' element limit (index.ts MAX_TOOL_INPUT_ELEMENTS), and the answer grows with the
|
|
32
|
+
// distinct purls sent, not with this.
|
|
33
|
+
export const MAX_PURL_ENTRIES = 10_000;
|
|
12
34
|
// CycloneDX nests components; this bounds the walk.
|
|
13
35
|
const MAX_DEPTH = 32;
|
|
14
36
|
// The order in which extract() reads and counts the distinct purls (#2799 review): purls as listed;
|
|
@@ -25,7 +47,7 @@ export const CHECK_SBOM_TITLE = "Check an SBOM against the advisory corpus";
|
|
|
25
47
|
export const CHECK_SBOM_ROUTING = "For container images and Kubernetes pods, the input is an SBOM of each image, or its purls.";
|
|
26
48
|
export const CHECK_SBOM_DESCRIPTION = "Check a dependency list against EchelonGraph's advisory corpus, one verdict per component. " +
|
|
27
49
|
CHECK_SBOM_ROUTING +
|
|
28
|
-
" Pass purls (package URLs, up to 2,000 distinct) or sbom (a CycloneDX or SPDX JSON document, up to 5,000,000 characters). This server reads the purls
|
|
50
|
+
" Pass purls (package URLs, up to 2,000 distinct, 10,000 with repeats) or sbom (a CycloneDX or SPDX JSON document, up to 5,000,000 characters). This server reads the document's purls; only they are sent to the API, in POST bodies of at most 200, never in a URL; the document itself is not sent on. Run from npm, this server is on your machine; over the hosted endpoint (mcp.echelongraph.io) it is EchelonGraph's, and the document is the request body, accepted up to 6 MiB. data.results holds one row per component sent, in order, with verdict (affected, not_affected, undetermined or not_assessed), not_assessed_reason, cve_ids, matched_package and matched_via (a deb or apk purl's upstream qualifier matches as its source package); each match carries fixed_in (the next unaffected version) or null with fixed_in_reason. data.summary counts the verdicts. not_affected is the only clean verdict: not_assessed (no verdict, as for a deb, apk or rpm purl without a distro qualifier naming its release, which EchelonGraph does not guess) and undetermined are not, and the note counts both. The API allows 1,200 components a minute per caller; on a 429 the tool waits out Retry-After, up to 50 seconds a call, then answers what it has, the rest in data.not_sent_purls, not clean. Input with more than 2,000 distinct purls is refused, not truncated." +
|
|
29
51
|
" " +
|
|
30
52
|
TEXT_BUDGET_DESCRIPTION +
|
|
31
53
|
" Cut, each row keeps index, purl, verdict, not_assessed_reason and cve_ids at least, each match its fixed_in while kept; not_affected rows leave the text first, then not_assessed ones; not_sent_purls keeps its first 10.";
|
|
@@ -576,9 +598,16 @@ export function registerCheckSbom(server, deps) {
|
|
|
576
598
|
title: CHECK_SBOM_TITLE,
|
|
577
599
|
description: CHECK_SBOM_DESCRIPTION,
|
|
578
600
|
inputSchema: z.object({
|
|
579
|
-
|
|
601
|
+
// #3345: bounded before any element is parsed (inputBounds.ts): MAX_PURL_ENTRIES entries,
|
|
602
|
+
// repeats included (the MAX_PURLS distinct a call checks is checkSbom's own rule, above),
|
|
603
|
+
// and an object document's top level to MAX_SBOM_MEMBERS.
|
|
604
|
+
purls: z
|
|
605
|
+
.array(z.string())
|
|
606
|
+
.max(MAX_PURL_ENTRIES)
|
|
607
|
+
.optional()
|
|
608
|
+
.describe("package URLs to check, e.g. pkg:npm/lodash@4.17.20 or pkg:deb/debian/openssl@3.0.11-1~deb12u1?distro=debian-12 (up to 2,000 distinct, sent in batches of 200), in a list of at most 10,000 entries, repeats included"),
|
|
580
609
|
sbom: z
|
|
581
|
-
.union([z.string(),
|
|
610
|
+
.union([z.string(), boundedRecord(z.unknown(), MAX_SBOM_MEMBERS)])
|
|
582
611
|
.optional()
|
|
583
612
|
.describe("a CycloneDX JSON or SPDX JSON document, as JSON text or as an object; its purls are read by this MCP server and only they are sent to the API; over the hosted endpoint (mcp.echelongraph.io) the document is the request body"),
|
|
584
613
|
full: FULL_ARG,
|
|
@@ -271,12 +271,14 @@ export function registerScanManifest(server, deps) {
|
|
|
271
271
|
title: SCAN_MANIFEST_TITLE,
|
|
272
272
|
description: SCAN_MANIFEST_DESCRIPTION,
|
|
273
273
|
inputSchema: z.object({
|
|
274
|
+
// #3345: at most MAX_FILES, checked before any file is parsed (inputBounds.ts says why).
|
|
274
275
|
files: z
|
|
275
276
|
.array(z.object({
|
|
276
277
|
filename: z.string().describe("the file's name, e.g. package-lock.json or services/api/go.mod; it picks the format unless format is set"),
|
|
277
278
|
content: z.string().describe("the file's text, as it is on disk"),
|
|
278
279
|
format: formatEnum.optional().describe("the format to read the file as, when its name does not say"),
|
|
279
280
|
}))
|
|
281
|
+
.max(MAX_FILES)
|
|
280
282
|
.describe("1 to 20 files, up to 5,000,000 characters of content in all; read by this MCP server into purls, and only the purls are sent to the API; over the hosted endpoint (mcp.echelongraph.io) the files are the request body"),
|
|
281
283
|
full: FULL_ARG,
|
|
282
284
|
}),
|
package/docs/rate-limits.md
CHANGED
|
@@ -38,8 +38,9 @@ once; a third waits up to 15 seconds for one to finish (two may wait at once), a
|
|
|
38
38
|
HTTP 503 with `Retry-After` and a JSON-RPC error (code `-32030`) only when that wait runs out, or at
|
|
39
39
|
once when two are already waiting. Each
|
|
40
40
|
instance serves at most 64 requests at once; the next waits up to 10 seconds for one to finish,
|
|
41
|
-
and is then answered the same way. A `subscriptions/listen` stream
|
|
42
|
-
|
|
41
|
+
and is then answered the same way. A `subscriptions/listen` stream is not one of the 64: the
|
|
42
|
+
server sends no list-change notification, so since 3.1.1 the stream ends right after its
|
|
43
|
+
acknowledgement, with a `complete` result (through 3.1.0 it stayed open until its client left).
|
|
43
44
|
|
|
44
45
|
A call whose client goes away (its own timeout, a closed tab) is stopped only once the server
|
|
45
46
|
learns of it. Its API calls are then cancelled, `check_sbom` and `scan_manifest` send no further batch, and its
|
|
@@ -53,3 +53,11 @@ made, with the same envelope as every other refusal: `state` `invalid_input`, `e
|
|
|
53
53
|
`invalid_input`, `isError` true, and notes that name each argument and the rule it broke (by its
|
|
54
54
|
JSON type, never by a value sent). Through 3.0.2 the MCP SDK answered those itself, with one
|
|
55
55
|
`Input validation error` text block and no `structuredContent` (#3015).
|
|
56
|
+
|
|
57
|
+
One call is refused before any tool sees it, so its result is not a tool's and carries no
|
|
58
|
+
`structuredContent`: since 3.1.1, arguments that hold more than 2,500,000 list entries and object
|
|
59
|
+
members in all, counted at every depth. That is the most any tool accepts, a `check_sbom` document
|
|
60
|
+
passed as an object at its 5,000,000-character cap, so no call a tool would answer is refused. The
|
|
61
|
+
MCP SDK counts them before any `inputSchema` is read (its `maxToolInputElements`, the fix for
|
|
62
|
+
CVE-2026-111768) and answers `isError` true with one text block of its own, "Invalid arguments for
|
|
63
|
+
tool <name>: arguments contain more than the maximum of 2500000 elements" (#3345).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "echelongraph-mcp",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"mcpName": "io.echelongraph/echelongraph-mcp",
|
|
5
5
|
"description": "MCP server for CVE lookups: EchelonGraph scores, CISA KEV, EPSS, CWE, exploits, version and SBOM checks, vendor advisories, and per-CVE exposure derived from Shodan data. Shodan data is owned by Shodan, which holds its copyright (© Shodan). Keyless.",
|
|
6
6
|
"type": "module",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"node": ">=20"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@modelcontextprotocol/server": "2.
|
|
67
|
-
"zod": "4.6.
|
|
66
|
+
"@modelcontextprotocol/server": "2.3.0",
|
|
67
|
+
"zod": "4.6.6"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@types/node": "^20.14.0",
|
package/server.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
]
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
|
-
"version": "3.1.
|
|
20
|
+
"version": "3.1.1",
|
|
21
21
|
"remotes": [
|
|
22
22
|
{
|
|
23
23
|
"type": "streamable-http",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
{
|
|
29
29
|
"registryType": "npm",
|
|
30
30
|
"identifier": "echelongraph-mcp",
|
|
31
|
-
"version": "3.1.
|
|
31
|
+
"version": "3.1.1",
|
|
32
32
|
"transport": {
|
|
33
33
|
"type": "stdio"
|
|
34
34
|
},
|