echelongraph-mcp 2.6.0 → 2.6.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 CHANGED
@@ -6,6 +6,14 @@ 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
+ ## 2.6.1 — 2026-10-04
10
+
11
+ - Hosted endpoint only: when the production synthetic calls `https://mcp.echelongraph.io/mcp`, the
12
+ endpoint marks its own API calls with a fixed `echelongraph-mcp-synthetic/1.0` token, so the
13
+ synthetic is never counted as hosted use. The client's own text is never passed through. The
14
+ `mcp_request` log line gains `client_public` (#2737).
15
+ - stdio behaviour is unchanged.
16
+
9
17
  ## 2.6.0 — 2026-10-04
10
18
 
11
19
  - `check_sbom` checks up to 2,000 distinct purls per call, instead of refusing more than 200: it
package/dist/http.js CHANGED
@@ -23,7 +23,7 @@
23
23
  // token fatal at start-up, because without it every remote user would share one API budget.
24
24
  import http from "node:http";
25
25
  import { createMcpHandler } from "@modelcontextprotocol/server";
26
- import { forwardToken, markHttpEntrypoint, withClient } from "./runtime.js";
26
+ import { forwardToken, markHttpEntrypoint, SYNTHETIC_UA_FAMILY, withClient } from "./runtime.js";
27
27
  import { BODY_TOO_LARGE_CODE, DEFAULT_RATE_LIMIT_PER_MIN, FixedWindowLimiter, ORIGIN_REFUSED_CODE, RATE_LIMITED_CODE, bodyFacts, originVerdict, resolveClient, revisionLabel, rpcError, uaFamily, } from "./httpPolicy.js";
28
28
  // Must run before index.ts is evaluated: it is how index.ts knows not to start stdio.
29
29
  markHttpEntrypoint();
@@ -158,7 +158,11 @@ async function serveMcp(req, res, pathname) {
158
158
  body: body && body.length > 0 ? new Uint8Array(body) : undefined,
159
159
  signal: abort.signal,
160
160
  });
161
- const response = await withClient(client.forwardAddress, () => handler.fetch(request));
161
+ // #2737: the production synthetic names itself by its user-agent family; its API calls then
162
+ // carry the synthetic's fixed token (runtime.ts upstreamUserAgent), so they are never counted
163
+ // as hosted adoption.
164
+ const synthetic = uaFamily(req.headers["user-agent"]) === SYNTHETIC_UA_FAMILY;
165
+ const response = await withClient(client.forwardAddress, () => handler.fetch(request), { synthetic });
162
166
  const out = { ...cors };
163
167
  response.headers.forEach((v, k) => {
164
168
  out[k] = v;
@@ -169,7 +173,7 @@ async function serveMcp(req, res, pathname) {
169
173
  res.write(chunk);
170
174
  }
171
175
  res.end();
172
- return { status: response.status, body };
176
+ return { status: response.status, body, clientPublic: client.forwardAddress !== undefined };
173
177
  }
174
178
  const server = http.createServer((req, res) => {
175
179
  const start = process.hrtime.bigint();
@@ -196,6 +200,7 @@ const server = http.createServer((req, res) => {
196
200
  throttled: o.throttled === true,
197
201
  refused: o.refused ?? "",
198
202
  ua_family: uaFamily(req.headers["user-agent"]),
203
+ client_public: o.clientPublic === true,
199
204
  });
200
205
  };
201
206
  if (pathname === HEALTH_PATH) {
package/dist/index.js CHANGED
@@ -52,7 +52,7 @@ import { registerCheckAffected } from "./tools/check_affected.js";
52
52
  import { registerCheckSbom } from "./tools/check_sbom.js";
53
53
  import { registerCveIntel } from "./tools/cve_intel.js";
54
54
  import { registerGetCwe } from "./tools/get_cwe.js";
55
- import { isHttpEntrypoint, upstreamHeaders } from "./runtime.js";
55
+ import { isHttpEntrypoint, upstreamHeaders, upstreamUserAgent } from "./runtime.js";
56
56
  import { registerVendorAdvisoryTools } from "./tools/vendor_advisories.js";
57
57
  import { registerPrompts } from "./prompts.js";
58
58
  import { registerResources } from "./resources.js";
@@ -186,8 +186,9 @@ export async function api(path, init) {
186
186
  method: init?.method,
187
187
  body: init?.body,
188
188
  // upstreamHeaders: empty on stdio; on the hosted endpoint, the forward token and the
189
- // end client's address (runtime.ts, #2316).
190
- headers: { ...upstreamHeaders(`${BASE}${path}`), ...(init?.headers ?? {}), "User-Agent": UA, Accept: "application/json" },
189
+ // end client's address (runtime.ts, #2316). upstreamUserAgent: UA, unless the hosted
190
+ // endpoint is answering the production synthetic (#2737).
191
+ headers: { ...upstreamHeaders(`${BASE}${path}`), ...(init?.headers ?? {}), "User-Agent": upstreamUserAgent(UA), Accept: "application/json" },
191
192
  signal: ctrl.signal,
192
193
  });
193
194
  }
package/dist/runtime.js CHANGED
@@ -22,8 +22,25 @@ export function isHttpEntrypoint() {
22
22
  // withClient(), and the SDK dispatches the tool handler inside that same async context, so
23
23
  // api() can read it without the tool module passing it through every function.
24
24
  const clientStore = new AsyncLocalStorage();
25
- export function withClient(clientIp, fn) {
26
- return clientStore.run({ clientIp }, fn);
25
+ export function withClient(clientIp, fn, opts = {}) {
26
+ return clientStore.run({ clientIp, synthetic: opts.synthetic === true }, fn);
27
+ }
28
+ // #2737: the production MCP synthetic probes this endpoint too, and its calls must reach the API
29
+ // under its own family, never as hosted MCP adoption. The synthetic names itself to this service
30
+ // with this family as its User-Agent's leading token; http.ts marks such a request (synthetic:
31
+ // true) and upstreamUserAgent() then puts this FIXED token ahead of the service's own user-agent,
32
+ // exactly as ECHELONGRAPH_MCP_UA does for the npm package. The client's text is never relayed: a
33
+ // request either carries this one constant or nothing. Anyone can claim the family, as anyone can
34
+ // send it to the API directly; the consequence is an undercounted adoption number, never a bypass
35
+ // (core-backend: user-agent classification is for counting only).
36
+ export const SYNTHETIC_UA_FAMILY = "echelongraph-mcp-synthetic";
37
+ export const SYNTHETIC_UA_TOKEN = `${SYNTHETIC_UA_FAMILY}/1.0`;
38
+ /** The User-Agent for one API call: `ua`, led by the synthetic's token when the request is the synthetic's. */
39
+ export function upstreamUserAgent(ua) {
40
+ if (!httpEntrypoint || clientStore.getStore()?.synthetic !== true)
41
+ return ua;
42
+ const lead = ua.split(/[/\s]/, 1)[0];
43
+ return lead === SYNTHETIC_UA_FAMILY ? ua : `${SYNTHETIC_UA_TOKEN} ${ua}`;
27
44
  }
28
45
  /** The header that carries the forward token, and the one that names the client (core-backend waf/mcpforward.go). */
29
46
  export const FORWARD_TOKEN_HEADER = "X-EG-MCP-Forward";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echelongraph-mcp",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
4
4
  "mcpName": "io.echelongraph/echelongraph-mcp",
5
5
  "description": "EchelonGraph CVE and internet-exposure data as an MCP server: CVEs with EchelonGraph multi-source scores, CISA-KEV and EPSS, plus a per-CVE internet-exposure footprint derived from Shodan data. Shodan data is owned by Shodan, which holds its copyright (© Shodan). Free and keyless, for Claude, Cursor and any MCP client.",
6
6
  "type": "module",
package/server.json CHANGED
@@ -17,7 +17,7 @@
17
17
  ]
18
18
  }
19
19
  ],
20
- "version": "2.6.0",
20
+ "version": "2.6.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": "2.6.0",
31
+ "version": "2.6.1",
32
32
  "transport": {
33
33
  "type": "stdio"
34
34
  },