EchelonGraph CVE & Exposure

io.echelongraphv2.3.4更新于 Oct 3, 2026

CVEs (NVD, CISA-KEV, EPSS, GHSA) and per-CVE exposure from Shodan data (© Shodan). Free, keyless.

概览

AI 生成的概览

让助手查询 EchelonGraph 免费公开数据源中的 CVE 严重性、利用情况和互联网暴露数据。

功能
提供五个只读工具,基于 EchelonGraph 的 CVE 与暴露数据:cve_summary 按严重性区间统计活跃 CVE 数量;search_cves 按严重性、CVSS、文本和排序筛选 CVE;get_cve 返回单个 CVE 的完整记录(CVSS v3/v4、EPSS、CISA-KEV 状态、勒索软件使用、GHSA 编号、参考链接);cve_exposure 返回某个 CVE 的互联网暴露足迹;exposure_radar 返回汇总数据,如 KEV 暴露、未认证数据存储、泄露凭据、影子 AI 和 MCP 服务器。结果原样转发 API 的 JSON,并附说明解释每个数字的含义。
适用场景
适合助手进行漏洞研判:判断某个 CVE 是否正被利用或已列入 KEV、比较严重性与 EPSS 分数,或评估某个 CVE 在记录中的互联网暴露服务数量。也适用于汇总暴露情况和泄露凭据统计。
运行要求
需要 Node.js 20 或更高版本;通过 npx 在本地运行,无需全局安装。无需 API 密钥或身份验证。可选环境变量 ECHELONGRAPH_API_BASE(默认 ECHELONGRAPH_API_TIMEOUT_MS(默认 15000)。需要访问 EchelonGraph API 的网络连接。
安装前请注意
只读且无需密钥,但会查询第三方 API,因此查询内容会发送给 EchelonGraph。暴露计数是基于样本的 banner 版本推断,不是漏洞利用测试,也不是全网普查;零值可能表示该 CVE 不在雷达跟踪范围内,而非没有暴露。未评分的 CVE 带有占位值,并非评分。泄露凭据数据是来自公开提交的、未经校验的凭据形字符串。

安装

在 SourceWeft 中

  1. 打开 控制台中的 EchelonGraph CVE & Exposure,将其添加到工作区。
  2. 为需要使用其工具的对话启用该服务。

Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。

其他 MCP 客户端

参照 仓库 中的启动说明。

README

EchelonGraph MCP server

CVE and internet-exposure data for Claude, Cursor, Cline, and any MCP client, straight from EchelonGraph's free public feed.

It exposes EchelonGraph's CVE Pulse (NVD + MITRE-CNA pre-NVD + CISA-KEV + EPSS + GitHub GHSA, fused into one score) plus a per-CVE internet-exposure footprint: how many internet-facing services (distinct ip:port) EchelonGraph's KEV-exposure radar has on record running a version that maps to the CVE. Exposure counts are derived from Shodan data. Shodan data is owned by Shodan, which holds its copyright (© Shodan).

Free and keyless: no API key, no auth, read-only. The server makes no request other than the API call a tool needs to answer.

Tools

ToolWhat it does
cve_summaryCounts of active CVEs by severity band, the count with no severity band from any source (summary.none, sent again as summary.unscored: CVEs not yet scored, not a rating of None), the same CVEs counted by NVD's severity label, as provenance (summary.nvd_critical to summary.nvd_none), the rejected (withdrawn) records outside the total (summary.rejected), and when the feed was last updated.
search_cvesSearch/filter CVEs (severity, min CVSS, text, sort) with EchelonGraph scores and score_assessed; the note names each row not yet scored.
get_cveFull record for one CVE: CVSS v3 and (when scored) v4, the EchelonGraph score and its confidence, whether EchelonGraph has scored it (score_assessed), EPSS, CISA-KEV status and known ransomware use, the GitHub GHSA id, references, and its published, modified and updated_at times.
cve_exposureInternet-exposure footprint for a CVE: exposed service count (distinct ip:port, the exposed_hosts field) + country/product breakdown, from the KEV-exposure radar.
exposure_radarAggregate totals across the exposure radars: services running CISA-KEV CVEs; unauthenticated data stores and observability UIs, found through Shodan (LeakIX when Shodan query credits run low) and then confirmed by EchelonGraph's own identified check, which is not a pure read (on Redis it names its client; on ClickHouse its query lands in the server's query log); leaked credentials; shadow AI; and MCP servers found in EchelonGraph's own Certificate Transparency feed, by RFC 9728 verdict, protocol era and transport. Every number is labelled by what it counts, and a field the tool cannot label is left out and named.

Every figure is as fresh as the schedule that refreshes it. The CVE feed is polled from its sources on a schedule; each radar refreshes on its own.

How cve_summary reads its counts

summary.critical, summary.high, summary.medium and summary.low count the active CVEs by severity band, and summary.total counts them all. summary.none counts the active CVEs with no severity band from any source: CVEs not yet scored, not CVEs rated None. The answer sends the same count again as summary.unscored.

summary.nvd_critical, summary.nvd_high, summary.nvd_medium, summary.nvd_low and summary.nvd_none count the same active CVEs a second time, by NVD's CVSS severity label (v3.x, else v4.0). Before NVD's record arrives, or where it gives none, a pre-NVD label from the CVE.org record or a GitHub advisory can stand in. They are provenance, never EchelonGraph's severity band. summary.nvd_none counts the active CVEs with no Critical, High, Medium or Low label there, and many of those carry an NVD CVSS v2 score instead, so it is neither a count of CVEs rated None nor the count of CVEs with no severity, which is summary.none.

summary.rejected counts the CVE records rejected (withdrawn) by their numbering authority. summary.total and the other counts leave them out, and they are withdrawn records, never vulnerabilities.

The tool relays the API's JSON as it was sent. When summary.none, summary.nvd_none or summary.rejected is above zero, the note says what it counts, with the number from the answer. The note says the five NVD counts add up to summary.total only when the answer's do, and says nothing about a field the answer does not carry.

How get_cve and search_cves read the EchelonGraph score

echelongraph_score (0 to 10), its band echelongraph_severity and the risk priority echelongraph_risk (0 to 100) are EchelonGraph's score only when the record's score_assessed is true. A CVE EchelonGraph has not scored carries score_assessed: false, and it is not yet scored, not scored 0. The API leaves those three fields out on it, or (an API before that change) sends 0, NONE and 0 as placeholders, which are not a rating and do not mean the CVE is harmless. Its score_confidence is NONE, and score_unassessed_reason says why: no source has yet published severity data EchelonGraph can score, or the record was rejected (withdrawn) by its numbering authority, and a rejected record is never scored.

Both tools relay the API's JSON as it was sent, and the note says what the score is, for get_cve per CVE and for search_cves per row, naming each CVE it is about:

API answerWhat the note says
score_assessed: trueNothing more: the score is a score.
score_assessed: falseNOT YET SCORED, tagged "(score_assessed: false)": report the CVE as not yet scored, never as a zero or low score. Each of echelongraph_score, echelongraph_severity and echelongraph_risk it carries is named as a placeholder. A rejected record is NOT SCORED instead.
no score_assessed fieldThe answer does not say whether the CVE was scored (an API older than the field), and a zero echelongraph_score in it is not a rating.

How cve_exposure counts

Every 12 hours, when Shodan query credits allow, the KEV-exposure radar runs one Shodan query per tracked product, reads up to 100 ip:port services per query, and keeps a service when its banner version matches a CISA-KEV or high-EPSS (>= 0.5) CVE. A service whose row has not been written or refreshed for 21 days is dropped. A count is therefore a banner-version inference over a sample: not an exploit test, and not an internet-wide census.

last_seen is when EchelonGraph last wrote or refreshed a service's row, not when the service was observed, and not when its vulnerable version was last confirmed. It is set to the time of the write when a search matches the banner, and again when a re-check finds the port still listed by Shodan InternetDB, without re-reading the banner, so a patched service can stay counted while its port stays open. Shodan's own time for the banner is not stored. So last_seen dates the write, never the sighting, and it is never measured_at (see "Structured results").

The unit is a service, not a machine. Shodan returns one banner per port and the radar keys each observation on ip:port, so a machine answering on two ports counts twice. The API's field names are kept (exposed_hosts here; distinct_hosts, ransomware_hosts and each ranked row's count in exposure_radar), but every one of them counts ip:port services.

The radar only looks for its tracked set of CVEs, so a zero means different things. The note tags each case with its exposure_state, as "(exposure_state: …)"; that tag says what the count is, and is not the result's state:

API answerWhat the tool says
tracked: true, exposed_hosts above 0exposure_state exposed: the count of services the radar has on record for the CVE.
tracked: falseexposure_state not_assessed, NOT ASSESSED: outside the radar's tracked set; 0 is not a measurement.
tracked: true, exposed_hosts: 0exposure_state measured_zero, a zero in the radar's sample: none of the up to 100 services it read per tracked-product query matched. Not an internet-wide zero, and undated: the answer does not say when the radar looked. The backend does not send this answer today.
no tracked fieldThe API did not say whether the CVE is in the tracked set: an older API, or the radar cannot decide (for example a CISA-KEV or high-EPSS CVE in a tracked product with 0 services on record). 0 exposed services on record, with no claim either way (exposure_state tracking_unknown).
HTTP 400, or an id that is not CVE-YYYY-NNNN…An error result tagged invalid_input; nothing was looked up.

Whatever the exposure_state, the result's state is not_assessed with measured_at null: the answer does not say when any service it counts was observed (see "Structured results").

How exposure_radar counts

exposure_radar relays each radar's answer cut to the fields it can label, and its note labels every number by what it counts. A field this version does not know is left out and the note names it, whether it is a total or a field inside a ranked row; so is a known field in an unexpected shape, and a list with one malformed row is left out whole, since a partial list reads as a complete one. kev_exposure, exposed_databases and leaked_credentials also carry generated_at, when the API computed their totals, and, when the API can tell, last_run_at: when that radar last completed a check (each table below says what a completed check is for that radar).

last_run_at is a timestamp, not a count. It is recorded for the radar as a whole, not for the server instance that answered, in UTC to the second. It moves only at the end of a cycle whose reads succeeded, so a cycle that read nothing leaves it where it was. It is not the time of every record a radar's numbers count: those cover everything still on record, not only what the last check found. Nor is it generated_at, which is only when the API recomputed the totals. The API omits last_run_at when it has no completed check on record or cannot read it; the result then carries none, and the note says nothing about it. When it is there, the note gives it as, for example, "kev_exposure last completed check: 2026-09-27T03:12:44Z".

kev_exposure

Every count is over the services the KEV-exposure radar keeps: a Shodan banner whose version maps to a CISA-KEV-listed CVE (see "How cve_exposure counts").

FieldWhat it counts
kev_exposure.distinct_hostsDistinct ip:port services, not machines, with at least one CISA-KEV-listed CVE on record. A machine answering on two ports counts twice.
kev_exposure.ransomware_hostsThe services among them with at least one ransomware-linked KEV CVE (one whose KEV entry notes known use in ransomware campaigns).
kev_exposure.kev_cves_exposedDistinct CISA-KEV-listed CVEs with at least one service on record.
kev_exposure.ransomware_cvesThe ransomware-linked CVEs among them.
kev_exposure.correlationsService×CVE pairs, not services: a service with three KEV CVEs counts three times.
kev_exposure.top_products, kev_exposure.top_countries, kev_exposure.top_cvesUp to 12 products, 10 countries and 12 CVEs, ranked by distinct ip:port services.
kev_exposure.top_cves[].cvss_v3_score, kev_exposure.top_cves[].epss_scoreScores, not counts: the highest CVSS v3 base score and EPSS probability (0 to 1) recorded on that CVE's observations.
kev_exposure.trendService×CVE pairs, not services, by the week (starting Monday) in which each pair was first recorded, over the last 12 weeks. Only pairs still on record are counted, so earlier weeks read low.
kev_exposure.newest_kevThe 15 CVEs that EchelonGraph's CVE records most recently mark as CISA-KEV-listed (by added_date), each with an exposure_state (below).
kev_exposure.newest_kev[].exposed_hostsOnly on a row whose exposure_state is exposed (or measured_zero): distinct ip:port services on record with that CVE.
kev_exposure.newest_kev[].cvss_v3_score, kev_exposure.newest_kev[].epss_scoreScores, not counts: the CVE record's CVSS v3 base score and EPSS probability (0 to 1), absent when the record has none.
kev_exposure.last_run_atA timestamp, not a count: when the radar last completed a check, a cycle in which its Shodan search answered at least one query (others may have failed) and its list of services due for a re-check was read. A cycle that skipped the search for want of Shodan query credits, or whose reads failed, does not move it.

A newest_kev row's exposure_state says whether its count is a measurement:

exposure_stateWhat it means
exposedThe radar has services on record with this CVE, and exposed_hosts counts them.
not_assessedNOT ASSESSED: the answer holds no measurement for this CVE, so no count is relayed. The API answers 0 for every CVE the radar holds no service for, whether or not the radar looks for that CVE at all, and it never marks a CVE as tracked with 0 services, so that 0 is not a measurement. cve_exposure says per CVE whether it is in the radar's tracked set.
measured_zeroOnly if the API ever marks a row tracked: true with 0 services: a zero in the radar's sample, not an internet-wide zero. If it marks a row tracked: false, that row is not_assessed whatever its count.
exposed_databases
FieldWhat it counts
exposed_databases.distinct_hostsDistinct ip:port services, not machines, that EchelonGraph's check confirmed answering without authentication: data stores and observability UIs alike.
exposed_databases.enginesDistinct engine types among them (for example redis or grafana).
exposed_databases.top_engines, exposed_databases.top_countriesUp to 15 engines and 10 countries, ranked by those services.
exposed_databases.pii_likely, exposed_databases.pci_likelyServices whose schema names pass a high-confidence gate for personal data, or for payment-card data. The names are index, database, table or field names in the Shodan or LeakIX banner or in the check's response, never record values; the gate passes one unambiguous term (such as ssn or cardholder) or two distinct indicators. It is a precision-first schema gate, not a census: a service whose names do not pass it is not counted, whatever it holds, so the other services are not shown to hold no such data.
exposed_databases.last_run_atA timestamp, not a count: when the radar last completed a check, a cycle in which its Shodan search, or the LeakIX fallback, answered at least one query (others may have failed), its list of services due for a re-check was read, and the scan opt-out register could be consulted. A cycle that searched nothing, or whose reads failed, does not move it.
leaked_credentials
FieldWhat it counts
leaked_credentials.total(repository, secret) pairs, not distinct secrets: a secret committed to three repositories counts three times.
leaked_credentials.distinct_secretsEach secret once.
leaked_credentials.distinct_reposPublic GitHub repositories with at least one.
leaked_credentials.top_providers, leaked_credentials.top_typesUp to 15 providers and secret types, ranked by (repository, secret) pairs.
leaked_credentials.last_run_atA timestamp, not a count: when the radar last completed a check, a cycle that read the public GitHub event stream (fetches of some of the commits it lists may have failed). A cycle whose read of that stream failed does not move it.

None of them is validated. Each is a credential-shaped string in a public commit that passed EchelonGraph's filters, at most structurally checked (a checksum or format decode), and never tested against its provider, so none of these is a count of working credentials.

shadow_ai

The shadow-AI radar's answer mixes numbers that count exposed services with numbers that count every observation. So exposure_radar regroups it by what each number counts, and its note labels each one. Only shadow_ai.confirmed_exposed counts exposed services.

FieldWhat it counts
confirmed_exposed.totalConfirmed exposed: services EchelonGraph's probes found answering without an authentication gate (liveness active, or rechecking during a re-check). It is the sum of confirmed_exposed.by_category.
confirmed_exposed.by_categoryThe same services, by category.
confirmed_exposed.last_24hConfirmed-exposed services first recorded in the last 24 hours.
observed.totalEvery Certificate Transparency or Shodan observation on record, whatever its verification state: observed, not exposed.
observed.by_categoryThe same observations, by category.
observed.last_24hObservations first recorded in the last 24 hours.
observed.trend_30dObservations per UTC day over the last 30 days.
observed.top_products, observed.top_countries, observed.top_issuersUp to ten products, countries and issuers, ranked by observations, not by exposed services. An issuer is the certificate's CA for a Certificate Transparency observation, and the hosting operator Shodan reports for a Shodan one.
observed.last_observationWhen the latest observation was recorded.
authentication.observedObservations where a probe observed an authentication gate: a 401/403, a login page or an auth marker.
authentication.not_determinedObservations whose service answered, but where no probe could tell whether it enforces authentication.

Neither authentication count is part of confirmed_exposed, and observed.total minus confirmed_exposed.total is not a count of secured services: it also holds observations not yet verified, whose hostname no longer resolves, or whose service no longer answers openly.

The tool relays no number it cannot label. A stats field this version does not know is left out, and the note names it; so is a count in an unexpected shape. A ranked row carries only its name and its count, and a field added to one is named too. The poller block's keys are checked the same way: a key this version does not know is left out and named, while the instance fields it knows are left out as described below.

mcp_servers

The AI-exposure radar's MCP-server verdicts, from GET /api/v1/public/ai-exposure/stats?service=mcp. Every hostname was named like an MCP server in EchelonGraph's own Certificate Transparency feed, matched by hostname pattern; no Shodan data is used. Each is checked by EchelonGraph's identified MCP probe: a server/discover request, and initialize only if that is refused. It never sends tools/call. The answer is counts and timestamps only, with no hostname, version or server name, and the tool repeats no string it carries but its timestamps. Each hostname is counted once, by its latest verdict on record, and EchelonGraph's own control servers are left out. The unit is a hostname, not an ip:port service.

FieldWhat it counts
mcp_servers.totalHostnames the radar has checked for an MCP server, each once, by its latest verdict on record. Not every one is an MCP server: see mcp_servers.not_assessed_by_reason. It is mcp_servers.protected + mcp_servers.pending_readjudication + mcp_servers.not_assessed.
mcp_servers.protectedHostnames whose /mcp endpoint asked for credentials (a 401 or 403) and whose OAuth protected-resource metadata validated under RFC 9728: a 200 JSON document whose resource is identical to the server's identifier and that names at least one authorization server.
mcp_servers.prm_via.header, mcp_servers.prm_via.wellknown_path, mcp_servers.prm_via.wellknown_rootThe protected ones, by where that document was found: the same-origin URL the credential challenge named; /.well-known/oauth-protected-resource followed by the endpoint's path; or /.well-known/oauth-protected-resource itself. They add up to mcp_servers.protected.
mcp_servers.pending_readjudicationVerdicts decided by a rule EchelonGraph has since replaced and not yet re-checked under the current rules. They are in neither mcp_servers.protected nor mcp_servers.not_assessed.
mcp_servers.not_assessedThe rest, whose protection the radar could not assess. Not assessed does not mean unprotected. mcp_servers.not_assessed_by_reason puts each of them in exactly one bucket (below).
mcp_servers.own_controls_excludedEchelonGraph's own control servers, left out of every other mcp_servers number.
mcp_servers.window.from, mcp_servers.window.toTimestamps, not counts: when the oldest and the newest of the verdicts counted were last checked. The counts are each hostname's latest verdict, not one sweep at one time. With no verdict counted there is no window.
mcp_servers.last_run_atA timestamp, not a count: when the AI-exposure radar, which checks other AI services as well as MCP servers, last completed a check (a cycle whose reads succeeded and whose scan opt-out register answered). It is not the time of every verdict counted.
mcp_servers.enabledWhether the API reports the radar running: true when a check completed within 45 minutes of its answer.
mcp_servers.counted_atA timestamp, not a count: when the API read these counts.

mcp_servers.not_assessed_by_reason divides mcp_servers.not_assessed:

BucketWhat it holds
mcp_servers.not_assessed_by_reason.identified_no_challengeServers that identified themselves as MCP servers (a DiscoverResult, an InitializeResult, or the endpoint event of the deprecated HTTP+SSE transport) and did not ask for credentials at the handshake. That is normal in MCP: authorization is optional in the spec, and a server can enforce it at tools/call instead, which EchelonGraph never sends. So this bucket is not a finding of exposure.
mcp_servers.not_assessed_by_reason.resource_mismatchAn endpoint that asked for credentials, whose metadata document's resource is absent or not identical to the server's identifier.
mcp_servers.not_assessed_by_reason.cross_origin_pointerAn endpoint that asked for credentials, whose challenge named a metadata URL on another origin, which EchelonGraph records and does not request.
mcp_servers.not_assessed_by_reason.bare_challenge_no_prmAn endpoint that asked for credentials, whose challenge named no metadata URL, and neither well-known URI answered 2xx.
mcp_servers.not_assessed_by_reason.pointer_unreachableAn endpoint that asked for credentials, whose same-origin metadata URL did not answer 2xx in full.
mcp_servers.not_assessed_by_reason.pointer_invalidAn endpoint that asked for credentials, whose challenge's metadata pointer is not a URL EchelonGraph will request.
mcp_servers.not_assessed_by_reason.no_authorization_serversAn endpoint that asked for credentials, whose metadata document names no authorization server.
mcp_servers.not_assessed_by_reason.wellknown_unreachableAn endpoint that asked for credentials, where a well-known metadata request got no complete HTTP answer.
mcp_servers.not_assessed_by_reason.metadata_invalidAn endpoint that asked for credentials, whose metadata answer is not a 200 with a JSON object within the size cap.
mcp_servers.not_assessed_by_reason.challenge_unadjudicatedAn endpoint that asked for credentials before EchelonGraph read RFC 9728 metadata, not re-checked since.
mcp_servers.not_assessed_by_reason.no_http_answerHostnames that gave no HTTP answer: DNS, TCP or TLS failed, or the request timed out. Not a count of MCP servers.
mcp_servers.not_assessed_by_reason.not_identified_as_mcpHostnames that answered HTTP with nothing that identified an MCP server: a login or error page, a body that is not JSON-RPC, a 4xx or 5xx. Not a count of MCP servers.

The eight credential-challenge buckets hold endpoints that asked for credentials: only their metadata did not validate, so none of them is shown to lack protection.

mcp_servers.era and mcp_servers.transport each divide every counted hostname, so each adds up to mcp_servers.total:

BucketWhat it holds
mcp_servers.era.legacyAn initialize-era server: an InitializeResult, or the endpoint event of the deprecated HTTP+SSE transport.
mcp_servers.era.dualA server that answered server/discover and itself named an initialize-era version too. A lower bound: a server built on the reference SDK names only modern versions there, and is counted modern.
mcp_servers.era.modernA server that answered server/discover and named no initialize-era version. Not proven modern-only: EchelonGraph does not send it initialize to tell.
mcp_servers.era.unknownNothing identified an era; every credential challenge is here.
mcp_servers.era.not_measuredA verdict recorded before EchelonGraph's probe began recording the era, not re-checked since.
mcp_servers.transport.streamable_httpIdentified by a POST to /mcp.
mcp_servers.transport.legacy_sseIdentified by the endpoint event of GET /sse, the deprecated HTTP+SSE transport.
mcp_servers.transport.unknownNothing identified a transport.
mcp_servers.transport.not_measuredRecorded before the era probe, not re-checked since.

The tool refuses the answer, as a failure of that radar and never as zeros, when it does not say service mcp (an API older than ?service=mcp ignores the parameter and answers its counts over every AI service it checks), when mcp_servers.total, mcp_servers.protected, mcp_servers.pending_readjudication or mcp_servers.not_assessed is missing or not a whole number, or when they contradict each other. A partition whose buckets are not exactly those above, each a whole number adding up to the count it divides, is left out whole and named. A field this version does not know is left out, and named only when its name is shaped like a field name.

What a result means

Every tool answers in one of two shapes, so a model reading the result cannot mistake an outage for an all-clear:

  • Success — the first text block is the API's JSON verbatim; the second is a one-line note saying the call succeeded, which base URL answered, and what it found; the third is the structured result as JSON, less what the first two already say (below). When the feed genuinely holds nothing for the query the note says so in words ("we looked and found nothing … not a lookup failure"), because a measured zero is a measurement. The exception to verbatim is exposure_radar: each radar is cut to the fields listed above, and each kev_exposure.newest_kev row gains an exposure_state. Its shadow_ai counts are regrouped as above, and its poller block carries only running and last_run_at, or is dropped when it carries no real completion time. The block's other fields describe the server instance that answered, not the radar. Its last_run_at is when the radar's leader last completed a Certificate Transparency (crt.sh) cycle, and its running is true only when that was within 30 minutes of the answer. When running is true, the note says the radar's leader last completed a cycle at that time; when it is false, the note says no cycle has completed since that time; when last_run_at is absent, the note says the radar's freshness is unknown and the block is left out. An older API answered from whichever server instance served the request, and a follower instance sent running: false with the zero time 0001-01-01T00:00:00Z; that too is unknown freshness, never presented as a stopped radar.
  • Failure — an MCP error result (isError: true) whenever the lookup did not complete: the host could not be reached, it answered non-2xx, it took longer than the timeout, or it answered 2xx with a body that is not a JSON object. The text names the tool, the cause (status code or error kind), the path, and the base URL, and says it is not a finding; the second text block is the structured result as JSON, less the message's sentences (below). A failure is never rendered as a success with null fields.

Both shapes also carry a structured result, below, and repeat it in their last text block, less what an earlier block already says, so a client that passes only content to the model still sees how the answer was measured. The first text block (the API's JSON, or the failure) and the note after it are where 1.x put them.

Structured results

Since 2.0.0 every result, success or failure, carries structuredContent, and every tool declares its shape as an outputSchema in tools/list, with a title and the annotations readOnlyHint: true, destructiveHint: false, idempotentHint: true and openWorldHint: true (hints, which a client treats as untrusted).

FieldWhat it holds
statemeasured, not_assessed, failed or invalid_input (below).
measured_atWhen the underlying observation was made, as the API states it. null when the answer does not say or holds no observation; never the Go zero time.
methodHow the numbers were produced. null on a failure.
coverageWhat the answer covers, where the tool can say: in_scope for cve_exposure, the list's own account of its count for search_cves, and the radars that answered for exposure_radar.
freshnessThe producing radar's last completed check (last_run_at), where the API serves one; null where it serves none.
notesThe caveats, one sentence each: what the envelope itself needs saying, then the note from the text block.
dataOn a success only: the same JSON as the first text block.
errorOn a failure only: kind, path, status and message.

The last text block of every result is this structured result serialized as JSON, less what an earlier text block already says verbatim, since a client may pass only content to the model. It carries the same state, measured_at, coverage and freshness (and on a failure error), key for key. It leaves out data, which is a success's first text block, and the note's sentences, which are the text block just before it (on a failure, the message) and with which notes ends: its notes holds only the sentences the envelope adds about itself, and is left out when there are none. It leaves out method too where that note quotes it verbatim, as cve_exposure's does ("Method: …"). So the text blocks together carry the whole structured result, and cannot disagree with it. Before 2.2.0 this block repeated the whole note, so every note was sent twice.

stateWhat it means
measuredA measurement of what was asked. An exposure count is measured only when the answer says when what it counts was observed (measured_at) and how it was produced (method). No exposure answer says when today, so neither exposure tool answers measured.
not_assessedThe answer holds no dated measurement of what was asked, so no count in it is presented as one: the radar does not look for this CVE, the answer does not say whether it does, or the answer does not say when what it counts was observed. It can still relay a count, as what the source holds on record, undated: cve_exposure's exposed_hosts when its exposure_state is exposed, and exposure_radar's labelled totals. The notes, and exposure_state where the result carries it, say what each count is.
failedThe lookup did not complete. Not a finding.
invalid_inputThe input was refused, by this server or by the API, so nothing was looked up. Not a finding.

Every field comes from what the API sends; where the API does not say, the field is null and a note says so. Per tool:

  • cve_summary is measured, and its measured_at is summary.last_updated, the newest modification time among the active CVE records it counts.
  • search_cves is measured, with measured_at null: each record carries its own times. Its coverage repeats the list's total, total_counted, total_is_lower_bound, search_relaxed, limit and offset, and adds returned, the rows in the page. When total_counted is false the total is not a count, and the note does not call it one.
  • get_cve is measured, and its measured_at is the record's updated_at, when EchelonGraph last wrote it.
  • cve_exposure is not_assessed with measured_at null, every answer: the per-CVE answer carries no time at which the services it counts were observed. Its last_seen is when EchelonGraph last wrote or refreshed one of their rows, not when any of them was observed, so it is never measured_at, and a note says so. A tracked zero is not_assessed too, since a zero has no observation to date it. The count is still relayed, labelled: exposure_state is exposed, measured_zero, not_assessed or tracking_unknown, the cases of "How cve_exposure counts", and coverage.in_scope is the API's tracked. It becomes measured only when the API serves a time at which the counted services were observed.
  • exposure_radar is not_assessed with measured_at null: every radar answered, but no answer gives one time at which what it counts was observed, so no count is presented as a dated measurement. mcp_servers dates its verdicts only by a window, mcp_servers.window.from to mcp_servers.window.to, over checks made at different times, which is not one observation time. Its numbers keep the labels above. freshness holds each radar's last_run_at where the API serves one, for shadow_ai also running, and for mcp_servers also enabled.

The CVE feed tools' freshness is null: the feed's answers carry no time at which its pollers last completed a poll for the feed as a whole.

A success whose fields do not fit the tool's outputSchema (a field of a type the schema does not allow) is returned as a failure with error.kind unexpected_shape, never relayed. A field the API adds later is still relayed by the four tools that relay the API's JSON, and exposure_radar leaves it out and names it, as above.

Protocol versions

The server answers both eras of the Model Context Protocol on stdio:

  • 2026-07-28: a client that opens with server/discover receives a DiscoverResult listing 2026-07-28, the tools capability and the server instructions, and then sends each request with the per-request _meta envelope.
  • 2025 and earlier: a client that opens with initialize, as every 1.x SDK client does, negotiates 2025-11-25, 2025-06-18, 2025-03-26 or 2024-11-05; a version the server does not know is answered with 2025-11-25.

The first message of a connection picks its era. The DiscoverResult lists only 2026-07-28, as the SDK builds it: the 2025-era versions are reached through initialize. Both handshakes carry the package's name and version (serverInfo), and every API request carries them in its User-Agent, echelongraph-mcp/<version>.

Both handshakes also carry the server instructions: the data is public; what state, measured_at and freshness mean, and that the last text block repeats them; that exposure numbers are aggregate counts of ip:port services, not an internet-wide census; and that Shodan data is Shodan's.

Install

Requires Node.js 20 or later. Add it to your MCP client's config. It runs via npx — no global install needed.

Claude Desktop

claude_desktop_config.json → mcpServers:

json
{  "mcpServers": {    "echelongraph": {      "command": "npx",      "args": ["-y", "echelongraph-mcp"]    }  }}

Cursor / Cline / Windsurf

~/.cursor/mcp.json (or the client's MCP settings):

json
{  "mcpServers": {    "echelongraph": {      "command": "npx",      "args": ["-y", "echelongraph-mcp"]    }  }}

Restart the client, then ask: "Is CVE-2023-44487 actively exploited, and how many exposed services does EchelonGraph's radar have on record for it?"

Configuration

Env varDefaultPurpose
ECHELONGRAPH_API_BASEhttps://app.echelongraph.ioOverride the API base (self-host / proxy).
ECHELONGRAPH_API_TIMEOUT_MS15000Per-request timeout. A slower answer is reported as a failed lookup, not as empty data.

Develop

bash
npm installnpm run build      # tsc → dist/npm test           # build, then the behavioural suite over both protocol eras, against a stub API (no network needed)npm run smoke      # spawn the server + call cve_exposure against the production API

npm run smoke calls the production API with this package's User-Agent, so its requests are counted as external MCP adoption. npm test never leaves the machine.

npm test runs the suite once over a 2026-07-28 server/discover and once over a 2025-06-18 initialize, plus the era tests. To run it against an installed package rather than dist/, set ECHELONGRAPH_MCP_BIN to that package's echelongraph-mcp bin: the tests then run the bin directly, as npx does, and read that package's own files.

License

The code is MIT-licensed.

The CVE Pulse compilation (how EchelonGraph combines its CVE sources, and the EchelonGraph score) is © EchelonGraph, served under the CVE Pulse free-access terms; the source records it compiles stay under their publishers' terms.

Exposure counts are derived from Shodan data. Shodan data is owned by Shodan, which holds its copyright (© Shodan). EchelonGraph claims no ownership of it or copyright in it.

来源:README.md,提交 54c58a4

工具

0
工具元数据尚未被收录。

版本历史

1
  1. v2.3.4最新Oct 3, 2026