@wrongstack/mcp 1.0.26 → 1.0.28
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/dist/client.d.ts +9 -1
- package/dist/index.js +131 -63
- package/dist/manage.d.ts +4 -0
- package/dist/transport-base.d.ts +5 -0
- package/package.json +2 -2
package/dist/client.d.ts
CHANGED
|
@@ -26,7 +26,15 @@ export declare class MCPClient {
|
|
|
26
26
|
* (stdio child exit, `close()`) so callers don't hang forever.
|
|
27
27
|
*/
|
|
28
28
|
private readonly pending;
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The unterminated tail of stdout, as the chunks it arrived in; joined only
|
|
31
|
+
* when its newline arrives. Kept as one string (`+=` then `indexOf`), every
|
|
32
|
+
* chunk flattened and re-scanned everything buffered so far — O(n²) for a
|
|
33
|
+
* multi-MiB tools/call result.
|
|
34
|
+
*/
|
|
35
|
+
private rxParts;
|
|
36
|
+
private get rxBuffer();
|
|
37
|
+
private set rxBuffer(value);
|
|
30
38
|
private rxBufferBytes;
|
|
31
39
|
/**
|
|
32
40
|
* Incremental UTF-8 decoder for the stdio rx path. A pipe read boundary can
|
package/dist/index.js
CHANGED
|
@@ -45,7 +45,9 @@ function parseMcpBearerChallenge(header, resource) {
|
|
|
45
45
|
const resourceMetadata = challengeParameter(parameters, "resource_metadata");
|
|
46
46
|
if (resourceMetadata) {
|
|
47
47
|
const metadataUrl = validateMetadataUrl(resourceMetadata);
|
|
48
|
-
if (metadataUrl
|
|
48
|
+
if (metadataUrl && isSameHost(metadataUrl, resource)) {
|
|
49
|
+
challenge.resourceMetadataUrl = metadataUrl;
|
|
50
|
+
}
|
|
49
51
|
}
|
|
50
52
|
const scope = challengeParameter(parameters, "scope");
|
|
51
53
|
if (scope) {
|
|
@@ -194,6 +196,13 @@ function validateMetadataUrl(value) {
|
|
|
194
196
|
return void 0;
|
|
195
197
|
}
|
|
196
198
|
}
|
|
199
|
+
function isSameHost(candidate, resource) {
|
|
200
|
+
try {
|
|
201
|
+
return unbracket(new URL(candidate).hostname).toLowerCase() === unbracket(new URL(resource).hostname).toLowerCase();
|
|
202
|
+
} catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
197
206
|
function record(value, label2) {
|
|
198
207
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
199
208
|
throw new Error(`MCP ${label2} must be an object`);
|
|
@@ -1543,27 +1552,8 @@ var SSEReader = class {
|
|
|
1543
1552
|
};
|
|
1544
1553
|
}
|
|
1545
1554
|
feed(chunk) {
|
|
1546
|
-
|
|
1547
|
-
throw new ToolError({
|
|
1548
|
-
message: `SSE: chunk size ${chunk.length} exceeds max buffer ${SSE_READER_MAX_BUFFER} \u2014 refusing to accumulate`,
|
|
1549
|
-
code: "TOOL_EXECUTION_FAILED",
|
|
1550
|
-
toolName: "mcp_transport_sse_reader",
|
|
1551
|
-
context: { phase: "feed", chunkLength: chunk.length, maxBuffer: SSE_READER_MAX_BUFFER }
|
|
1552
|
-
});
|
|
1553
|
-
}
|
|
1555
|
+
const carried = this.buffer.length;
|
|
1554
1556
|
this.buffer += chunk;
|
|
1555
|
-
if (this.buffer.length > SSE_READER_MAX_BUFFER) {
|
|
1556
|
-
throw new ToolError({
|
|
1557
|
-
message: `SSE: pending line exceeds ${SSE_READER_MAX_BUFFER} bytes \u2014 upstream is not framing events`,
|
|
1558
|
-
code: "TOOL_EXECUTION_FAILED",
|
|
1559
|
-
toolName: "mcp_transport_sse_reader",
|
|
1560
|
-
context: {
|
|
1561
|
-
phase: "feed",
|
|
1562
|
-
bufferLength: this.buffer.length,
|
|
1563
|
-
maxBuffer: SSE_READER_MAX_BUFFER
|
|
1564
|
-
}
|
|
1565
|
-
});
|
|
1566
|
-
}
|
|
1567
1557
|
let start = 0;
|
|
1568
1558
|
let idx = this.buffer.indexOf("\n", start);
|
|
1569
1559
|
while (idx !== -1) {
|
|
@@ -1574,6 +1564,25 @@ var SSEReader = class {
|
|
|
1574
1564
|
idx = this.buffer.indexOf("\n", start);
|
|
1575
1565
|
}
|
|
1576
1566
|
if (start > 0) this.buffer = this.buffer.slice(start);
|
|
1567
|
+
if (this.buffer.length <= SSE_READER_MAX_BUFFER) return;
|
|
1568
|
+
if (start >= carried) {
|
|
1569
|
+
throw new ToolError({
|
|
1570
|
+
message: `SSE: chunk size ${chunk.length} exceeds max buffer ${SSE_READER_MAX_BUFFER} \u2014 refusing to accumulate`,
|
|
1571
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
1572
|
+
toolName: "mcp_transport_sse_reader",
|
|
1573
|
+
context: { phase: "feed", chunkLength: chunk.length, maxBuffer: SSE_READER_MAX_BUFFER }
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
throw new ToolError({
|
|
1577
|
+
message: `SSE: pending line exceeds ${SSE_READER_MAX_BUFFER} bytes \u2014 upstream is not framing events`,
|
|
1578
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
1579
|
+
toolName: "mcp_transport_sse_reader",
|
|
1580
|
+
context: {
|
|
1581
|
+
phase: "feed",
|
|
1582
|
+
bufferLength: this.buffer.length,
|
|
1583
|
+
maxBuffer: SSE_READER_MAX_BUFFER
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1577
1586
|
}
|
|
1578
1587
|
processLine(line) {
|
|
1579
1588
|
if (line === "") {
|
|
@@ -1649,6 +1658,7 @@ import * as https2 from "node:https";
|
|
|
1649
1658
|
import { ConfigError as ConfigError2 } from "@wrongstack/core/types";
|
|
1650
1659
|
|
|
1651
1660
|
// src/elicitation.ts
|
|
1661
|
+
import { sanitizeTerminalText } from "@wrongstack/core/utils";
|
|
1652
1662
|
var ELICITATION_MAX_WAIT_MS = 10 * 6e4;
|
|
1653
1663
|
var MAX_FIELDS = 32;
|
|
1654
1664
|
var MAX_MESSAGE_CHARS = 4e3;
|
|
@@ -1659,6 +1669,13 @@ function isRecord(v) {
|
|
|
1659
1669
|
function optionalString3(v) {
|
|
1660
1670
|
return typeof v === "string" && v.trim() ? v : void 0;
|
|
1661
1671
|
}
|
|
1672
|
+
function displayText(v) {
|
|
1673
|
+
return sanitizeTerminalText(v);
|
|
1674
|
+
}
|
|
1675
|
+
function optionalDisplayString(v) {
|
|
1676
|
+
const text = optionalString3(v);
|
|
1677
|
+
return text === void 0 ? void 0 : optionalString3(displayText(text));
|
|
1678
|
+
}
|
|
1662
1679
|
function optionalNumber(v) {
|
|
1663
1680
|
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
1664
1681
|
}
|
|
@@ -1672,7 +1689,7 @@ function parseOptions(schema) {
|
|
|
1672
1689
|
const names = Array.isArray(schema["enumNames"]) ? schema["enumNames"] : [];
|
|
1673
1690
|
return values.map((value, i) => ({
|
|
1674
1691
|
value,
|
|
1675
|
-
label: typeof names[i] === "string" && names[i] ? names[i] : value
|
|
1692
|
+
label: displayText(typeof names[i] === "string" && names[i] ? names[i] : value)
|
|
1676
1693
|
}));
|
|
1677
1694
|
}
|
|
1678
1695
|
const titled = Array.isArray(schema["oneOf"]) ? schema["oneOf"] : Array.isArray(schema["anyOf"]) ? schema["anyOf"] : void 0;
|
|
@@ -1682,7 +1699,7 @@ function parseOptions(schema) {
|
|
|
1682
1699
|
if (!isRecord(entry) || typeof entry["const"] !== "string") return void 0;
|
|
1683
1700
|
options.push({
|
|
1684
1701
|
value: entry["const"],
|
|
1685
|
-
label:
|
|
1702
|
+
label: optionalDisplayString(entry["title"]) ?? displayText(entry["const"])
|
|
1686
1703
|
});
|
|
1687
1704
|
}
|
|
1688
1705
|
return options;
|
|
@@ -1691,8 +1708,8 @@ function parseField(name, schema, required) {
|
|
|
1691
1708
|
if (!isRecord(schema)) return { ok: false, error: `property "${name}" is not a schema object` };
|
|
1692
1709
|
const base = {
|
|
1693
1710
|
name,
|
|
1694
|
-
title:
|
|
1695
|
-
description:
|
|
1711
|
+
title: optionalDisplayString(schema["title"]),
|
|
1712
|
+
description: optionalDisplayString(schema["description"]),
|
|
1696
1713
|
required
|
|
1697
1714
|
};
|
|
1698
1715
|
const type = schema["type"];
|
|
@@ -1789,7 +1806,7 @@ function parseUrlElicitation(params) {
|
|
|
1789
1806
|
ok: true,
|
|
1790
1807
|
value: {
|
|
1791
1808
|
mode: "url",
|
|
1792
|
-
message: message.slice(0, MAX_MESSAGE_CHARS),
|
|
1809
|
+
message: displayText(message).slice(0, MAX_MESSAGE_CHARS),
|
|
1793
1810
|
url: url.href,
|
|
1794
1811
|
elicitationId
|
|
1795
1812
|
}
|
|
@@ -1822,10 +1839,10 @@ function parseElicitationParams(params) {
|
|
|
1822
1839
|
if (!parsed.ok) return parsed;
|
|
1823
1840
|
fields.push(parsed.value);
|
|
1824
1841
|
}
|
|
1825
|
-
return { ok: true, value: { message: message.slice(0, MAX_MESSAGE_CHARS), fields } };
|
|
1842
|
+
return { ok: true, value: { message: displayText(message).slice(0, MAX_MESSAGE_CHARS), fields } };
|
|
1826
1843
|
}
|
|
1827
1844
|
function label(field) {
|
|
1828
|
-
return field.title ?? field.name;
|
|
1845
|
+
return field.title ?? displayText(field.name);
|
|
1829
1846
|
}
|
|
1830
1847
|
function checkValue(field, raw) {
|
|
1831
1848
|
const name = label(field);
|
|
@@ -1884,7 +1901,7 @@ function validateElicitationContent(fields, content) {
|
|
|
1884
1901
|
out[field.name] = [];
|
|
1885
1902
|
continue;
|
|
1886
1903
|
}
|
|
1887
|
-
if (raw === void 0 || raw === null ||
|
|
1904
|
+
if (raw === void 0 || raw === null || Array.isArray(raw) && raw.length === 0) {
|
|
1888
1905
|
if (field.required) return { ok: false, error: `${label(field)} is required` };
|
|
1889
1906
|
continue;
|
|
1890
1907
|
}
|
|
@@ -2191,6 +2208,14 @@ if (!pinnedAgentsCleanupRegistered) {
|
|
|
2191
2208
|
pinnedAgents.clear();
|
|
2192
2209
|
});
|
|
2193
2210
|
}
|
|
2211
|
+
function urlOriginForLog(raw) {
|
|
2212
|
+
try {
|
|
2213
|
+
const url = new URL(raw);
|
|
2214
|
+
return `${url.protocol}//${url.host}`;
|
|
2215
|
+
} catch {
|
|
2216
|
+
return "<invalid url>";
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2194
2219
|
function makeAbortError(method) {
|
|
2195
2220
|
const err = new Error(`MCP request "${method}" aborted by client`);
|
|
2196
2221
|
err.name = "AbortError";
|
|
@@ -2262,15 +2287,16 @@ var BaseHTTPTransport = class {
|
|
|
2262
2287
|
this.requestTimeout = opts.requestTimeoutMs ?? 6e4;
|
|
2263
2288
|
if (opts.tls) {
|
|
2264
2289
|
if (opts.tls.rejectUnauthorized === false) {
|
|
2290
|
+
const where = urlOriginForLog(this.url);
|
|
2265
2291
|
if (!isTlsUnsafeAllowed()) {
|
|
2266
2292
|
throw new ConfigError2({
|
|
2267
|
-
message: `[mcp:${transportName}] TLS verification disabled \u2014 set WRONGSTACK_UNSAFE_MCP_TLS=1 to allow. Rejecting insecure configuration for ${
|
|
2293
|
+
message: `[mcp:${transportName}] TLS verification disabled \u2014 set WRONGSTACK_UNSAFE_MCP_TLS=1 to allow. Rejecting insecure configuration for ${where}.`,
|
|
2268
2294
|
code: "CONFIG_INVALID",
|
|
2269
|
-
context: { field: "tls.rejectUnauthorized", transportName, url:
|
|
2295
|
+
context: { field: "tls.rejectUnauthorized", transportName, url: where }
|
|
2270
2296
|
});
|
|
2271
2297
|
}
|
|
2272
2298
|
console.error(
|
|
2273
|
-
`[mcp:${transportName}] \u26A0\uFE0F TLS verification DISABLED for ${
|
|
2299
|
+
`[mcp:${transportName}] \u26A0\uFE0F TLS verification DISABLED for ${where}. Network attacks are possible \u2014 only use on localhost.`
|
|
2274
2300
|
);
|
|
2275
2301
|
}
|
|
2276
2302
|
this.tlsAgent = new https2.Agent({
|
|
@@ -3581,7 +3607,19 @@ var MCPClient = class _MCPClient {
|
|
|
3581
3607
|
* (stdio child exit, `close()`) so callers don't hang forever.
|
|
3582
3608
|
*/
|
|
3583
3609
|
pending = /* @__PURE__ */ new Map();
|
|
3584
|
-
|
|
3610
|
+
/**
|
|
3611
|
+
* The unterminated tail of stdout, as the chunks it arrived in; joined only
|
|
3612
|
+
* when its newline arrives. Kept as one string (`+=` then `indexOf`), every
|
|
3613
|
+
* chunk flattened and re-scanned everything buffered so far — O(n²) for a
|
|
3614
|
+
* multi-MiB tools/call result.
|
|
3615
|
+
*/
|
|
3616
|
+
rxParts = [];
|
|
3617
|
+
get rxBuffer() {
|
|
3618
|
+
return this.rxParts.join("");
|
|
3619
|
+
}
|
|
3620
|
+
set rxBuffer(value) {
|
|
3621
|
+
this.rxParts = value ? [value] : [];
|
|
3622
|
+
}
|
|
3585
3623
|
rxBufferBytes = 0;
|
|
3586
3624
|
/**
|
|
3587
3625
|
* Incremental UTF-8 decoder for the stdio rx path. A pipe read boundary can
|
|
@@ -4063,11 +4101,10 @@ var MCPClient = class _MCPClient {
|
|
|
4063
4101
|
}
|
|
4064
4102
|
}
|
|
4065
4103
|
onData(s) {
|
|
4066
|
-
this.rxBuffer += s;
|
|
4067
4104
|
this.rxBufferBytes += Buffer.byteLength(s, "utf8");
|
|
4068
4105
|
if (this.rxBufferBytes > _MCPClient.MAX_RX_BUFFER_BYTES) {
|
|
4069
4106
|
const truncated = this.rxBufferBytes;
|
|
4070
|
-
this.
|
|
4107
|
+
this.rxParts = [];
|
|
4071
4108
|
this.rxBufferBytes = 0;
|
|
4072
4109
|
this.failPending(
|
|
4073
4110
|
`MCP "${this.opts.name}" rx buffer overflow (${truncated} bytes without a newline) \u2014 closing connection`
|
|
@@ -4076,17 +4113,22 @@ var MCPClient = class _MCPClient {
|
|
|
4076
4113
|
return;
|
|
4077
4114
|
}
|
|
4078
4115
|
let start = 0;
|
|
4079
|
-
let idx =
|
|
4116
|
+
let idx = s.indexOf("\n");
|
|
4117
|
+
if (idx === -1) {
|
|
4118
|
+
this.rxParts.push(s);
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
4080
4121
|
while (idx !== -1) {
|
|
4081
|
-
const
|
|
4122
|
+
const head = this.rxParts.length > 0 ? this.rxParts.join("") : "";
|
|
4123
|
+
this.rxParts = [];
|
|
4124
|
+
const line = (head + s.slice(start, idx)).trim();
|
|
4082
4125
|
start = idx + 1;
|
|
4083
4126
|
if (line) this.onLine(line);
|
|
4084
|
-
idx =
|
|
4085
|
-
}
|
|
4086
|
-
if (start > 0) {
|
|
4087
|
-
this.rxBufferBytes -= Buffer.byteLength(this.rxBuffer.slice(0, start), "utf8");
|
|
4088
|
-
this.rxBuffer = this.rxBuffer.slice(start);
|
|
4127
|
+
idx = s.indexOf("\n", start);
|
|
4089
4128
|
}
|
|
4129
|
+
const tail = s.slice(start);
|
|
4130
|
+
this.rxBufferBytes = Buffer.byteLength(tail, "utf8");
|
|
4131
|
+
if (tail) this.rxParts.push(tail);
|
|
4090
4132
|
}
|
|
4091
4133
|
onLine(line) {
|
|
4092
4134
|
let msg;
|
|
@@ -4424,6 +4466,7 @@ function requireIdentity(value, label2) {
|
|
|
4424
4466
|
// src/elicitation-form.ts
|
|
4425
4467
|
import { randomUUID } from "node:crypto";
|
|
4426
4468
|
import { hostname } from "node:os";
|
|
4469
|
+
import { sanitizeTerminalText as sanitizeTerminalText2 } from "@wrongstack/core/utils";
|
|
4427
4470
|
var MAX_ATTEMPTS = 3;
|
|
4428
4471
|
var CONFIRM_QUESTION = "confirm";
|
|
4429
4472
|
var URL_QUESTION = "url";
|
|
@@ -4456,7 +4499,8 @@ function describe(field) {
|
|
|
4456
4499
|
function toQuestion(field, index) {
|
|
4457
4500
|
const base = {
|
|
4458
4501
|
id: questionId(index),
|
|
4459
|
-
|
|
4502
|
+
// Titles arrive sanitized from the parser; a bare field name does not.
|
|
4503
|
+
prompt: field.title ?? sanitizeTerminalText2(field.name),
|
|
4460
4504
|
description: describe(field),
|
|
4461
4505
|
required: field.required
|
|
4462
4506
|
};
|
|
@@ -4572,8 +4616,12 @@ function toUserInputRequest(request3, problem) {
|
|
|
4572
4616
|
function answerValue(field, answer) {
|
|
4573
4617
|
if (!answer || answer.delegated) return void 0;
|
|
4574
4618
|
switch (field.kind) {
|
|
4575
|
-
case "string":
|
|
4576
|
-
|
|
4619
|
+
case "string": {
|
|
4620
|
+
const text = answer.text;
|
|
4621
|
+
if (text === void 0) return void 0;
|
|
4622
|
+
if (!text.trim() && !field.required) return void 0;
|
|
4623
|
+
return text;
|
|
4624
|
+
}
|
|
4577
4625
|
case "number":
|
|
4578
4626
|
case "integer":
|
|
4579
4627
|
return answer.text?.trim() || void 0;
|
|
@@ -4660,8 +4708,14 @@ async function readConfig(path2) {
|
|
|
4660
4708
|
async function writeConfig(path2, cfg) {
|
|
4661
4709
|
const raw = JSON.stringify(cfg, null, 2);
|
|
4662
4710
|
const tmp = `${path2}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`;
|
|
4663
|
-
|
|
4711
|
+
let mode = 384;
|
|
4712
|
+
try {
|
|
4713
|
+
mode = (await fs.stat(path2)).mode & 511;
|
|
4714
|
+
} catch {
|
|
4715
|
+
}
|
|
4664
4716
|
try {
|
|
4717
|
+
await fs.writeFile(tmp, raw, { encoding: "utf8", mode });
|
|
4718
|
+
await fs.chmod(tmp, mode);
|
|
4665
4719
|
await fs.rename(tmp, path2);
|
|
4666
4720
|
} catch (err) {
|
|
4667
4721
|
await fs.rm(tmp, { force: true });
|
|
@@ -4733,6 +4787,10 @@ function buildConfig(input, base) {
|
|
|
4733
4787
|
if (allowedTools !== void 0) cfg.allowedTools = allowedTools;
|
|
4734
4788
|
const permission = input.permission ?? base?.permission;
|
|
4735
4789
|
if (permission !== void 0) cfg.permission = permission;
|
|
4790
|
+
const startupTimeoutMs = input.startupTimeoutMs ?? base?.startupTimeoutMs;
|
|
4791
|
+
if (startupTimeoutMs !== void 0) cfg.startupTimeoutMs = startupTimeoutMs;
|
|
4792
|
+
const requestTimeoutMs = input.requestTimeoutMs ?? base?.requestTimeoutMs;
|
|
4793
|
+
if (requestTimeoutMs !== void 0) cfg.requestTimeoutMs = requestTimeoutMs;
|
|
4736
4794
|
const allowPrivateNetworks = input.allowPrivateNetworks ?? base?.allowPrivateNetworks;
|
|
4737
4795
|
if (allowPrivateNetworks !== void 0) cfg.allowPrivateNetworks = allowPrivateNetworks;
|
|
4738
4796
|
const enabled = input.enabled ?? base?.enabled;
|
|
@@ -5091,52 +5149,59 @@ function healthStateFor(connectionState, operations, enabled = true) {
|
|
|
5091
5149
|
function evaluateHealthThresholds(operations, thresholds) {
|
|
5092
5150
|
if (!thresholds) return [];
|
|
5093
5151
|
const checks = [];
|
|
5094
|
-
|
|
5152
|
+
const connectionP95 = thresholds.connectionLatencyP95Ms;
|
|
5153
|
+
if (isUsableThreshold(connectionP95) && operations.connectionSamples.length > 0) {
|
|
5095
5154
|
const value = percentile(
|
|
5096
5155
|
[...operations.connectionSamples].sort((a, b) => a - b),
|
|
5097
5156
|
0.95
|
|
5098
5157
|
);
|
|
5099
5158
|
checks.push({
|
|
5100
5159
|
name: "connection-latency-p95",
|
|
5101
|
-
passed: value <=
|
|
5160
|
+
passed: value <= connectionP95,
|
|
5102
5161
|
value,
|
|
5103
|
-
threshold:
|
|
5162
|
+
threshold: connectionP95
|
|
5104
5163
|
});
|
|
5105
5164
|
}
|
|
5106
|
-
|
|
5165
|
+
const discoveryP95 = thresholds.discoveryLatencyP95Ms;
|
|
5166
|
+
if (isUsableThreshold(discoveryP95) && operations.discoverySamples.length > 0) {
|
|
5107
5167
|
const value = percentile(
|
|
5108
5168
|
[...operations.discoverySamples].sort((a, b) => a - b),
|
|
5109
5169
|
0.95
|
|
5110
5170
|
);
|
|
5111
5171
|
checks.push({
|
|
5112
5172
|
name: "discovery-latency-p95",
|
|
5113
|
-
passed: value <=
|
|
5173
|
+
passed: value <= discoveryP95,
|
|
5114
5174
|
value,
|
|
5115
|
-
threshold:
|
|
5175
|
+
threshold: discoveryP95
|
|
5116
5176
|
});
|
|
5117
5177
|
}
|
|
5118
|
-
|
|
5178
|
+
const callP95 = thresholds.callLatencyP95Ms;
|
|
5179
|
+
if (isUsableThreshold(callP95) && operations.callSamples.length > 0) {
|
|
5119
5180
|
const value = percentile(
|
|
5120
5181
|
[...operations.callSamples].sort((a, b) => a - b),
|
|
5121
5182
|
0.95
|
|
5122
5183
|
);
|
|
5123
5184
|
checks.push({
|
|
5124
5185
|
name: "call-latency-p95",
|
|
5125
|
-
passed: value <=
|
|
5186
|
+
passed: value <= callP95,
|
|
5126
5187
|
value,
|
|
5127
|
-
threshold:
|
|
5188
|
+
threshold: callP95
|
|
5128
5189
|
});
|
|
5129
5190
|
}
|
|
5130
|
-
|
|
5191
|
+
const inFlight = thresholds.inFlightCalls;
|
|
5192
|
+
if (isUsableThreshold(inFlight)) {
|
|
5131
5193
|
checks.push({
|
|
5132
5194
|
name: "in-flight-calls",
|
|
5133
|
-
passed: operations.peakInFlightCalls <=
|
|
5195
|
+
passed: operations.peakInFlightCalls <= inFlight,
|
|
5134
5196
|
value: operations.peakInFlightCalls,
|
|
5135
|
-
threshold:
|
|
5197
|
+
threshold: inFlight
|
|
5136
5198
|
});
|
|
5137
5199
|
}
|
|
5138
5200
|
return checks;
|
|
5139
5201
|
}
|
|
5202
|
+
function isUsableThreshold(value) {
|
|
5203
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
5204
|
+
}
|
|
5140
5205
|
function applyHealthThresholds(state, checks) {
|
|
5141
5206
|
if (state !== "healthy") return state;
|
|
5142
5207
|
return checks.some((c) => !c.passed) ? "degraded" : "healthy";
|
|
@@ -5648,7 +5713,8 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
5648
5713
|
slot.reconnectPending = false;
|
|
5649
5714
|
ctx.events.emit("mcp.server.disconnected", {
|
|
5650
5715
|
name: slot.cfg.name,
|
|
5651
|
-
reason: err instanceof Error ? err.message : "unknown"
|
|
5716
|
+
reason: err instanceof Error ? err.message : "unknown",
|
|
5717
|
+
terminal: true
|
|
5652
5718
|
});
|
|
5653
5719
|
return;
|
|
5654
5720
|
}
|
|
@@ -5869,7 +5935,8 @@ function scheduleRegistryReconnect({
|
|
|
5869
5935
|
);
|
|
5870
5936
|
events.emit("mcp.server.disconnected", {
|
|
5871
5937
|
name: slot.cfg.name,
|
|
5872
|
-
reason: `reconnect-exhausted:${slot.reconnectCycles}
|
|
5938
|
+
reason: `reconnect-exhausted:${slot.reconnectCycles}`,
|
|
5939
|
+
terminal: true
|
|
5873
5940
|
});
|
|
5874
5941
|
return;
|
|
5875
5942
|
}
|
|
@@ -6861,6 +6928,7 @@ function serveStdio(server, opts = {}) {
|
|
|
6861
6928
|
let bufferBytes = 0;
|
|
6862
6929
|
let closed = false;
|
|
6863
6930
|
let bufferTooLarge = false;
|
|
6931
|
+
const decoder = new TextDecoder();
|
|
6864
6932
|
let writeChain = Promise.resolve();
|
|
6865
6933
|
const inFlightHandlers = /* @__PURE__ */ new Set();
|
|
6866
6934
|
const writeLine = (s) => {
|
|
@@ -6883,7 +6951,7 @@ function serveStdio(server, opts = {}) {
|
|
|
6883
6951
|
};
|
|
6884
6952
|
const onData = (chunk) => {
|
|
6885
6953
|
if (bufferTooLarge) return;
|
|
6886
|
-
const text = typeof chunk === "string" ? chunk :
|
|
6954
|
+
const text = typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
|
|
6887
6955
|
buffer += text;
|
|
6888
6956
|
bufferBytes += Buffer.byteLength(text, "utf8");
|
|
6889
6957
|
if (bufferBytes > HTTP_BODY_CAP) {
|
|
@@ -7174,8 +7242,8 @@ var MCPVaultTokenStore = class {
|
|
|
7174
7242
|
async readFile() {
|
|
7175
7243
|
let raw;
|
|
7176
7244
|
try {
|
|
7177
|
-
const
|
|
7178
|
-
if (
|
|
7245
|
+
const stat3 = await fs3.stat(this.filePath);
|
|
7246
|
+
if (stat3.size > MAX_STORE_BYTES) throw new Error("MCP token store exceeds size limit");
|
|
7179
7247
|
raw = await fs3.readFile(this.filePath, "utf8");
|
|
7180
7248
|
} catch (error) {
|
|
7181
7249
|
if (error.code === "ENOENT") return emptyFile();
|
package/dist/manage.d.ts
CHANGED
|
@@ -17,6 +17,10 @@ export interface McpServerInput {
|
|
|
17
17
|
bearerTokenEnv?: string | undefined;
|
|
18
18
|
allowedTools?: string[] | undefined;
|
|
19
19
|
permission?: Permission | undefined;
|
|
20
|
+
/** Connect/startup timeout in ms. Defaults to 10s when omitted. */
|
|
21
|
+
startupTimeoutMs?: number | undefined;
|
|
22
|
+
/** Per-request JSON-RPC timeout in ms. Defaults to 60s when omitted. */
|
|
23
|
+
requestTimeoutMs?: number | undefined;
|
|
20
24
|
/** Lazy connect — spawn the process only on first tool call (see config). */
|
|
21
25
|
lazy?: boolean | undefined;
|
|
22
26
|
/** Env var names to forward from parent process at spawn time. */
|
package/dist/transport-base.d.ts
CHANGED
|
@@ -51,6 +51,11 @@ export interface HttpTransportOptions {
|
|
|
51
51
|
* Abort error whose `name` is `'AbortError'` so the core executor's
|
|
52
52
|
* classifyToolError maps it to FATAL / not-retryable (user cancellation).
|
|
53
53
|
*/
|
|
54
|
+
/**
|
|
55
|
+
* `scheme://host:port` of a server URL, for log lines. The path and query can
|
|
56
|
+
* hold an expanded `${SECRET}` (config-env), and userinfo is a credential.
|
|
57
|
+
*/
|
|
58
|
+
export declare function urlOriginForLog(raw: string): string;
|
|
54
59
|
export declare function makeAbortError(method: string): Error;
|
|
55
60
|
/** Advance a JSON-RPC number id without leaving JavaScript's safe-integer range. */
|
|
56
61
|
export declare function nextJsonRpcId(id: number): number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "WrongStack Model Context Protocol client and registry: stdio, SSE, and streamable HTTP transports.",
|
|
6
6
|
"repository": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"!dist/**/*.map"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@wrongstack/core": "1.0.
|
|
29
|
+
"@wrongstack/core": "1.0.28",
|
|
30
30
|
"undici": "^8.10.2"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|