echelongraph-mcp 3.0.2 → 3.1.0
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 +603 -5
- package/README.md +77 -82
- package/dist/http.js +73 -81
- package/dist/httpPolicy.js +197 -10
- package/dist/index.js +451 -44
- package/dist/logLines.js +86 -0
- package/dist/prompts.js +121 -22
- package/dist/readBody.js +140 -0
- package/dist/requiredText.js +23 -3
- package/dist/resources.js +3 -3
- package/dist/runtime.js +66 -1
- package/dist/schemaRefusal.js +138 -0
- package/dist/textBudget.js +826 -76
- package/dist/tools/check_affected.js +19 -6
- package/dist/tools/check_sbom.js +191 -22
- package/dist/tools/cve_intel.js +145 -35
- package/dist/tools/cve_remediation.js +101 -19
- package/dist/tools/epss_history.js +41 -5
- package/dist/tools/match_batch.js +57 -10
- package/dist/tools/packageRecords.js +58 -0
- package/dist/tools/scan_manifest.js +11 -4
- package/dist/tools/vendor_advisories.js +27 -4
- package/docs/exposure-radar.md +4 -0
- package/docs/rate-limits.md +6 -3
- package/docs/structured-results.md +15 -3
- package/docs/tools.md +139 -6
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/runtime.js
CHANGED
|
@@ -20,7 +20,72 @@ export function isHttpEntrypoint() {
|
|
|
20
20
|
}
|
|
21
21
|
const clientStore = new AsyncLocalStorage();
|
|
22
22
|
export function withClient(clientIp, fn, opts = {}) {
|
|
23
|
-
return clientStore.run({ clientIp, synthetic: opts.synthetic === true, signal: opts.signal, work: opts.work }, fn);
|
|
23
|
+
return clientStore.run({ clientIp, synthetic: opts.synthetic === true, signal: opts.signal, work: opts.work, rpcMethod: opts.rpcMethod, tool: opts.tool, protocolVersion: opts.protocolVersion }, fn);
|
|
24
|
+
}
|
|
25
|
+
/** #2923: the fixed labels of the request being served, for a line the SDK's onerror writes (it is handed only the
|
|
26
|
+
* error): the JSON-RPC method (MCP_METHODS or "(other)") and the protocol-version label; "(none)" outside a request. */
|
|
27
|
+
export function requestLabels() {
|
|
28
|
+
const scope = clientStore.getStore();
|
|
29
|
+
return { rpcMethod: scope?.rpcMethod ?? "(none)", protocolVersion: scope?.protocolVersion ?? "(none)" };
|
|
30
|
+
}
|
|
31
|
+
let upstreamSink;
|
|
32
|
+
/** http.ts only: where each failed API call is reported. */
|
|
33
|
+
export function onUpstreamFailure(sink) {
|
|
34
|
+
upstreamSink = sink;
|
|
35
|
+
}
|
|
36
|
+
/** Reports one failed API call: on the hosted endpoint to http.ts's line; on stdio, nowhere. */
|
|
37
|
+
export function upstreamFailed(f) {
|
|
38
|
+
if (!httpEntrypoint || !upstreamSink)
|
|
39
|
+
return;
|
|
40
|
+
const scope = clientStore.getStore();
|
|
41
|
+
try {
|
|
42
|
+
upstreamSink({ ...f, tool: scope?.tool ?? "(none)", rpcMethod: scope?.rpcMethod ?? "(none)", synthetic: scope?.synthetic === true });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// A log line never fails the call it describes.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const DNS_CODES = new Set(["ENOTFOUND", "EAI_AGAIN", "EAI_FAIL", "EAI_NONAME", "EAI_NODATA"]);
|
|
49
|
+
const RESET_CODES = new Set(["ECONNRESET", "EPIPE", "ECONNABORTED", "UND_ERR_SOCKET"]);
|
|
50
|
+
const CONNECT_TIMEOUT_CODES = new Set(["UND_ERR_CONNECT_TIMEOUT", "ETIMEDOUT"]);
|
|
51
|
+
const TIMEOUT_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT"]);
|
|
52
|
+
const UNREACHABLE_CODES = new Set(["EHOSTUNREACH", "ENETUNREACH", "EHOSTDOWN", "ENETDOWN"]);
|
|
53
|
+
const TLS_CODE = /^(?:ERR_TLS_|ERR_SSL_|ERR_OSSL_|CERT_|UNABLE_TO_|DEPTH_ZERO_SELF_SIGNED_CERT$|SELF_SIGNED_CERT_IN_CHAIN$|HOSTNAME_MISMATCH$)/;
|
|
54
|
+
/** The class of a fetch that threw: from the first code in its cause chain this knows, else network. Codes only, never a message. */
|
|
55
|
+
export function networkFailureClass(e) {
|
|
56
|
+
const codes = [];
|
|
57
|
+
let c = e;
|
|
58
|
+
for (let depth = 0; c instanceof Error && depth < 5; depth++) {
|
|
59
|
+
const x = c;
|
|
60
|
+
if (typeof x.code === "string")
|
|
61
|
+
codes.push(x.code);
|
|
62
|
+
// A connection tried on several addresses fails as an AggregateError of each one's error.
|
|
63
|
+
if (Array.isArray(x.errors)) {
|
|
64
|
+
for (const y of x.errors) {
|
|
65
|
+
const code = y?.code;
|
|
66
|
+
if (typeof code === "string")
|
|
67
|
+
codes.push(code);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
c = x.cause;
|
|
71
|
+
}
|
|
72
|
+
for (const code of codes) {
|
|
73
|
+
if (DNS_CODES.has(code))
|
|
74
|
+
return "dns";
|
|
75
|
+
if (code === "ECONNREFUSED")
|
|
76
|
+
return "refused";
|
|
77
|
+
if (RESET_CODES.has(code))
|
|
78
|
+
return "reset";
|
|
79
|
+
if (CONNECT_TIMEOUT_CODES.has(code))
|
|
80
|
+
return "connect_timeout";
|
|
81
|
+
if (TIMEOUT_CODES.has(code))
|
|
82
|
+
return "timeout";
|
|
83
|
+
if (UNREACHABLE_CODES.has(code))
|
|
84
|
+
return "unreachable";
|
|
85
|
+
if (TLS_CODE.test(code))
|
|
86
|
+
return "tls";
|
|
87
|
+
}
|
|
88
|
+
return "network";
|
|
24
89
|
}
|
|
25
90
|
// #2775: a hosted client can go before its answer is ready (its own timeout, a closed tab, a
|
|
26
91
|
// retry). Nobody will read that answer, so the request's API calls are cancelled (index.ts api()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const REFUSED = Symbol("echelongraph-mcp.schema-refused");
|
|
2
|
+
const isRefused = (v) => typeof v === "object" && v !== null && REFUSED in v;
|
|
3
|
+
// How many issues the note names one by one; the rest are counted.
|
|
4
|
+
const ISSUES_NAMED = 5;
|
|
5
|
+
const keyOf = (p) => (typeof p === "object" && p !== null ? p.key : p);
|
|
6
|
+
// files[0].filename, as a model reads an argument path.
|
|
7
|
+
function pathText(path) {
|
|
8
|
+
let out = "";
|
|
9
|
+
for (const p of path ?? []) {
|
|
10
|
+
const k = keyOf(p);
|
|
11
|
+
out += typeof k === "number" ? `[${k}]` : out === "" ? String(k) : `.${String(k)}`;
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
// The value the caller sent at a path, to name its JSON type (never the value itself).
|
|
16
|
+
function valueAt(input, path) {
|
|
17
|
+
let v = input;
|
|
18
|
+
for (const p of path ?? []) {
|
|
19
|
+
if (v === null || typeof v !== "object")
|
|
20
|
+
return undefined;
|
|
21
|
+
v = v[keyOf(p)];
|
|
22
|
+
}
|
|
23
|
+
return v;
|
|
24
|
+
}
|
|
25
|
+
function jsonType(v) {
|
|
26
|
+
if (v === null)
|
|
27
|
+
return "null";
|
|
28
|
+
if (Array.isArray(v))
|
|
29
|
+
return "an array";
|
|
30
|
+
if (typeof v === "number")
|
|
31
|
+
return Number.isInteger(v) ? "an integer" : "a number";
|
|
32
|
+
if (typeof v === "object")
|
|
33
|
+
return "an object";
|
|
34
|
+
return typeof v === "string" ? "a string" : typeof v === "boolean" ? "a boolean" : typeof v;
|
|
35
|
+
}
|
|
36
|
+
const TYPE_NAMES = {
|
|
37
|
+
string: "a string",
|
|
38
|
+
number: "a number",
|
|
39
|
+
int: "an integer",
|
|
40
|
+
boolean: "a boolean",
|
|
41
|
+
array: "an array",
|
|
42
|
+
object: "an object",
|
|
43
|
+
record: "an object",
|
|
44
|
+
bigint: "an integer",
|
|
45
|
+
};
|
|
46
|
+
const listOf = (items) => (items.length < 2 ? items.join("") : `${items.slice(0, -1).join(", ")} or ${items[items.length - 1]}`);
|
|
47
|
+
const bound = (n) => (typeof n === "bigint" ? n.toString() : typeof n === "number" ? String(n) : "?");
|
|
48
|
+
// One issue as "<argument> <rule>": the argument named by its path in the arguments, the rule in the
|
|
49
|
+
// inputSchema's terms. An issue this does not know is given in the schema library's own words.
|
|
50
|
+
export function issueText(issue, input) {
|
|
51
|
+
const at = pathText(issue.path);
|
|
52
|
+
const name = at === "" ? "the arguments" : at;
|
|
53
|
+
const sent = valueAt(input, issue.path);
|
|
54
|
+
switch (issue.code) {
|
|
55
|
+
case "invalid_type": {
|
|
56
|
+
const want = TYPE_NAMES[issue.expected ?? ""] ?? issue.expected ?? "another type";
|
|
57
|
+
if (sent === undefined)
|
|
58
|
+
return `${name} is required (${want})`;
|
|
59
|
+
if (issue.expected === "int" && typeof sent === "number")
|
|
60
|
+
return `${name} must be an integer`;
|
|
61
|
+
return `${name} must be ${want}, not ${jsonType(sent)}`;
|
|
62
|
+
}
|
|
63
|
+
case "too_big":
|
|
64
|
+
case "too_small": {
|
|
65
|
+
const big = issue.code === "too_big";
|
|
66
|
+
const n = bound(big ? issue.maximum : issue.minimum);
|
|
67
|
+
const cmp = issue.inclusive === false ? (big ? "less than" : "more than") : big ? "at most" : "at least";
|
|
68
|
+
if (issue.origin === "string")
|
|
69
|
+
return `${name} must be ${cmp} ${n} ${n === "1" ? "character" : "characters"} long`;
|
|
70
|
+
if (issue.origin === "array" || issue.origin === "set")
|
|
71
|
+
return `${name} must hold ${cmp} ${n} ${n === "1" ? "entry" : "entries"}`;
|
|
72
|
+
return `${name} must be ${cmp} ${n}`;
|
|
73
|
+
}
|
|
74
|
+
case "invalid_value":
|
|
75
|
+
return Array.isArray(issue.values) && issue.values.length
|
|
76
|
+
? `${name} must be ${issue.values.length === 1 ? "" : "one of "}${listOf(issue.values.map((v) => JSON.stringify(v)))}`
|
|
77
|
+
: `${name}: ${issue.message}`;
|
|
78
|
+
case "unrecognized_keys": {
|
|
79
|
+
const keys = (issue.keys ?? []).map((k) => (at === "" ? k : `${at}.${k}`));
|
|
80
|
+
return keys.length ? `${listOf(keys)} ${keys.length === 1 ? "is not an argument" : "are not arguments"} this tool takes` : `${name}: ${issue.message}`;
|
|
81
|
+
}
|
|
82
|
+
case "invalid_format":
|
|
83
|
+
return `${name} must be ${issue.format ? `a valid ${issue.format}` : "in the format the inputSchema gives"}`;
|
|
84
|
+
case "invalid_union": {
|
|
85
|
+
// Each branch refused only the value's type: name the types the branches take.
|
|
86
|
+
const types = (issue.errors ?? []).map((b) => (b.length === 1 && b[0].code === "invalid_type" && !b[0].path?.length ? TYPE_NAMES[b[0].expected ?? ""] : undefined));
|
|
87
|
+
const named = types.length > 0 && types.every((t) => t !== undefined) ? [...new Set(types)] : undefined;
|
|
88
|
+
return named ? `${name} must be ${listOf(named)}, not ${jsonType(sent)}` : `${name} must take one of the forms the inputSchema allows, not ${jsonType(sent)}`;
|
|
89
|
+
}
|
|
90
|
+
default:
|
|
91
|
+
return `${name}: ${issue.message}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// The reason a refusal gives, as one clause: each issue in turn, then how many more there are.
|
|
95
|
+
export function refusalReason(issues, input) {
|
|
96
|
+
const named = issues.slice(0, ISSUES_NAMED).map((i) => issueText(i, input));
|
|
97
|
+
const more = issues.length - named.length;
|
|
98
|
+
return `the arguments do not fit this tool's inputSchema (tools/list): ${named.join("; ")}${more > 0 ? `; and ${more} more ${more === 1 ? "issue" : "issues"}` : ""}`;
|
|
99
|
+
}
|
|
100
|
+
// The inputSchema a tool is registered with: `schema` itself for tools/list (its own jsonSchema,
|
|
101
|
+
// so the published JSON is the original's), and a validate that answers what `schema` answers for
|
|
102
|
+
// arguments it accepts, and a Refused marker, not the issues, for arguments it refuses.
|
|
103
|
+
export function lenientSchema(schema) {
|
|
104
|
+
const std = schema["~standard"];
|
|
105
|
+
const settle = (value, r) => (r.issues && r.issues.length ? { value: { [REFUSED]: { issues: r.issues, input: value } } } : r);
|
|
106
|
+
return {
|
|
107
|
+
"~standard": {
|
|
108
|
+
version: std.version,
|
|
109
|
+
vendor: std.vendor,
|
|
110
|
+
jsonSchema: std.jsonSchema,
|
|
111
|
+
validate: (value) => {
|
|
112
|
+
const r = std.validate(value);
|
|
113
|
+
return r instanceof Promise ? r.then((x) => settle(value, x)) : settle(value, r);
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const isStandardSchema = (s) => typeof s === "object" && s !== null && typeof s["~standard"]?.validate === "function";
|
|
119
|
+
/**
|
|
120
|
+
* Makes every tool `server` registers from now on answer arguments that break its inputSchema with
|
|
121
|
+
* `refuse(tool, reason)` (index.ts badInput: the invalid_input envelope), not the SDK's bare error.
|
|
122
|
+
* Call it before the first registerTool. tools/list is unchanged.
|
|
123
|
+
*/
|
|
124
|
+
export function withSchemaRefusal(server, refuse) {
|
|
125
|
+
const register = server.registerTool.bind(server);
|
|
126
|
+
const wrapped = (name, config, cb) => {
|
|
127
|
+
if (!isStandardSchema(config.inputSchema))
|
|
128
|
+
return register(name, config, cb);
|
|
129
|
+
return register(name, { ...config, inputSchema: lenientSchema(config.inputSchema) }, (args, ctx) => {
|
|
130
|
+
if (!isRefused(args))
|
|
131
|
+
return cb(args, ctx);
|
|
132
|
+
const { issues, input } = args[REFUSED];
|
|
133
|
+
return refuse(name, refusalReason(issues, input));
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
server.registerTool = wrapped;
|
|
137
|
+
return server;
|
|
138
|
+
}
|