@wrongstack/mcp 0.319.0 → 0.320.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/dist/client.d.ts +2 -0
- package/dist/index.js +215 -85
- package/dist/registry-disconnect.d.ts +8 -1
- package/package.json +2 -2
package/dist/client.d.ts
CHANGED
|
@@ -63,6 +63,7 @@ export declare class MCPClient {
|
|
|
63
63
|
private _toolsCache?;
|
|
64
64
|
private _drainPending;
|
|
65
65
|
private _lastNotifySkipped;
|
|
66
|
+
private closePromise?;
|
|
66
67
|
private sseTransport?;
|
|
67
68
|
private httpTransport?;
|
|
68
69
|
/** Notified when the stdio child process exits so the registry can attempt reconnect. */
|
|
@@ -106,6 +107,7 @@ export declare class MCPClient {
|
|
|
106
107
|
listPrompts(opts?: MCPPageOptions): Promise<MCPListPromptsResult>;
|
|
107
108
|
getPrompt(name: string, args?: Record<string, string> | undefined, opts?: MCPRequestOptions): Promise<MCPGetPromptResult>;
|
|
108
109
|
close(): Promise<void>;
|
|
110
|
+
private closeInner;
|
|
109
111
|
private request;
|
|
110
112
|
private requestCapability;
|
|
111
113
|
private requireResourceSubscriptions;
|
package/dist/index.js
CHANGED
|
@@ -1335,6 +1335,7 @@ function createTimeoutSignal(parent, timeoutMs) {
|
|
|
1335
1335
|
() => ctrl.abort(new Error(`MCP HTTP request timed out after ${timeoutMs}ms`)),
|
|
1336
1336
|
timeoutMs
|
|
1337
1337
|
);
|
|
1338
|
+
timer.unref?.();
|
|
1338
1339
|
return {
|
|
1339
1340
|
signal: ctrl.signal,
|
|
1340
1341
|
dispose: () => {
|
|
@@ -1775,7 +1776,8 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1775
1776
|
sseReader.feed(chunk);
|
|
1776
1777
|
}
|
|
1777
1778
|
} catch {
|
|
1778
|
-
|
|
1779
|
+
} finally {
|
|
1780
|
+
if (!this.readerDone && this.state !== "disconnected" && this.state !== "failed") {
|
|
1779
1781
|
this.state = "disconnected";
|
|
1780
1782
|
this.notifyDisconnect();
|
|
1781
1783
|
}
|
|
@@ -1809,9 +1811,13 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1809
1811
|
try {
|
|
1810
1812
|
const res = await this.fetchWithAuthorization(this.url, fetchOpts, timeoutSignal.signal);
|
|
1811
1813
|
if (!res.ok) {
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1814
|
+
let snippet;
|
|
1815
|
+
try {
|
|
1816
|
+
snippet = await readBodyCapped(res, MCP_CONSTANTS.REQUEST_LOG_CAP);
|
|
1817
|
+
} catch (err) {
|
|
1818
|
+
const received = err instanceof ToolError4 && typeof err.context?.["received"] === "number" ? err.context["received"] : void 0;
|
|
1819
|
+
snippet = typeof received === "number" ? `\u2026 [${received}+ bytes total]` : "\u2026 [error body unreadable]";
|
|
1820
|
+
}
|
|
1815
1821
|
throw new ToolError4({
|
|
1816
1822
|
message: `HTTP ${res.status}: ${snippet}`,
|
|
1817
1823
|
code: "TOOL_EXECUTION_FAILED",
|
|
@@ -1819,6 +1825,10 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1819
1825
|
context: { transport: "sse", url: this.url, status: res.status }
|
|
1820
1826
|
});
|
|
1821
1827
|
}
|
|
1828
|
+
if (method.startsWith("notifications/")) {
|
|
1829
|
+
await readBodyCapped(res).catch(() => void 0);
|
|
1830
|
+
return { jsonrpc: "2.0", id };
|
|
1831
|
+
}
|
|
1822
1832
|
let data;
|
|
1823
1833
|
try {
|
|
1824
1834
|
data = JSON.parse(await readBodyCapped(res));
|
|
@@ -1897,6 +1907,10 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1897
1907
|
}
|
|
1898
1908
|
});
|
|
1899
1909
|
}
|
|
1910
|
+
if (method.startsWith("notifications/")) {
|
|
1911
|
+
await readBodyCapped(res).catch(() => void 0);
|
|
1912
|
+
return { jsonrpc: "2.0", id };
|
|
1913
|
+
}
|
|
1900
1914
|
let data;
|
|
1901
1915
|
try {
|
|
1902
1916
|
data = JSON.parse(await readBodyCapped(res));
|
|
@@ -2023,10 +2037,10 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
2023
2037
|
const contentType = initRes.headers.get("content-type") ?? "";
|
|
2024
2038
|
let data;
|
|
2025
2039
|
if (contentType.includes("application/json")) {
|
|
2026
|
-
const parsed = await initRes
|
|
2040
|
+
const parsed = JSON.parse(await readBodyCapped(initRes));
|
|
2027
2041
|
if (isJsonRpcResult(parsed)) data = parsed;
|
|
2028
2042
|
} else {
|
|
2029
|
-
data = extractJsonRpcResults(await initRes
|
|
2043
|
+
data = extractJsonRpcResults(await readBodyCapped(initRes))[0];
|
|
2030
2044
|
}
|
|
2031
2045
|
if (!data) {
|
|
2032
2046
|
throw new Error("Could not parse initialize response");
|
|
@@ -2079,7 +2093,7 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
2079
2093
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
2080
2094
|
}
|
|
2081
2095
|
if (method.startsWith("notifications/")) {
|
|
2082
|
-
await res
|
|
2096
|
+
await readBodyCapped(res).catch(() => void 0);
|
|
2083
2097
|
return { jsonrpc: "2.0", id };
|
|
2084
2098
|
}
|
|
2085
2099
|
const match = this.consumeResponseText(await readBodyCapped(res), id);
|
|
@@ -2126,7 +2140,7 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
2126
2140
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
2127
2141
|
}
|
|
2128
2142
|
if (method.startsWith("notifications/")) {
|
|
2129
|
-
await res
|
|
2143
|
+
await readBodyCapped(res).catch(() => void 0);
|
|
2130
2144
|
return { jsonrpc: "2.0", id };
|
|
2131
2145
|
}
|
|
2132
2146
|
const parsed = this.consumeResponseText(await readBodyCapped(res), id);
|
|
@@ -2207,6 +2221,7 @@ var MCPClient = class _MCPClient {
|
|
|
2207
2221
|
_toolsCache;
|
|
2208
2222
|
_drainPending = false;
|
|
2209
2223
|
_lastNotifySkipped = false;
|
|
2224
|
+
closePromise;
|
|
2210
2225
|
// HTTP transports
|
|
2211
2226
|
sseTransport;
|
|
2212
2227
|
httpTransport;
|
|
@@ -2260,15 +2275,21 @@ var MCPClient = class _MCPClient {
|
|
|
2260
2275
|
async connect() {
|
|
2261
2276
|
this.state = "connecting";
|
|
2262
2277
|
this._serverMetadata = void 0;
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2278
|
+
try {
|
|
2279
|
+
if (this.opts.transport === "stdio") {
|
|
2280
|
+
await this.connectStdio();
|
|
2281
|
+
} else if (this.opts.transport === "sse") {
|
|
2282
|
+
await this.connectSSE();
|
|
2283
|
+
} else if (this.opts.transport === "streamable-http") {
|
|
2284
|
+
await this.connectStreamableHTTP();
|
|
2285
|
+
} else {
|
|
2286
|
+
throw new Error(`Unknown transport "${this.opts.transport}"`);
|
|
2287
|
+
}
|
|
2288
|
+
} catch (err) {
|
|
2289
|
+
await this.close().catch(() => {
|
|
2290
|
+
});
|
|
2270
2291
|
this.state = "failed";
|
|
2271
|
-
throw
|
|
2292
|
+
throw err;
|
|
2272
2293
|
}
|
|
2273
2294
|
}
|
|
2274
2295
|
async connectStdio() {
|
|
@@ -2303,6 +2324,13 @@ var MCPClient = class _MCPClient {
|
|
|
2303
2324
|
})() : spawn2(this.opts.command, rawArgs, { env: spawnEnv, stdio, windowsHide: true });
|
|
2304
2325
|
this.child = child;
|
|
2305
2326
|
child.stdout?.on("data", (chunk) => this.onData(chunk.toString()));
|
|
2327
|
+
child.stdout?.on("end", () => {
|
|
2328
|
+
if (this.rxBuffer.trim()) {
|
|
2329
|
+
const line = this.rxBuffer.trim();
|
|
2330
|
+
this.rxBuffer = "";
|
|
2331
|
+
this.onLine(line);
|
|
2332
|
+
}
|
|
2333
|
+
});
|
|
2306
2334
|
child.stderr?.on("data", () => {
|
|
2307
2335
|
});
|
|
2308
2336
|
child.stdin?.on("error", (err) => {
|
|
@@ -2554,6 +2582,13 @@ var MCPClient = class _MCPClient {
|
|
|
2554
2582
|
);
|
|
2555
2583
|
}
|
|
2556
2584
|
async close() {
|
|
2585
|
+
if (this.closePromise) return this.closePromise;
|
|
2586
|
+
this.closePromise = this.closeInner().finally(() => {
|
|
2587
|
+
this.closePromise = void 0;
|
|
2588
|
+
});
|
|
2589
|
+
return this.closePromise;
|
|
2590
|
+
}
|
|
2591
|
+
async closeInner() {
|
|
2557
2592
|
if (this.child) {
|
|
2558
2593
|
const child = this.child;
|
|
2559
2594
|
const exitPromise = new Promise((resolve) => {
|
|
@@ -2570,16 +2605,26 @@ var MCPClient = class _MCPClient {
|
|
|
2570
2605
|
}
|
|
2571
2606
|
const GRACEFUL_MS = 800;
|
|
2572
2607
|
const FORCE_TIMEOUT_MS = 1200;
|
|
2608
|
+
let gracefulTimer;
|
|
2573
2609
|
const gracefulRace = await Promise.race([
|
|
2574
2610
|
exitPromise.then(() => "exited"),
|
|
2575
|
-
new Promise((resolve) =>
|
|
2611
|
+
new Promise((resolve) => {
|
|
2612
|
+
gracefulTimer = setTimeout(() => resolve("timeout"), GRACEFUL_MS);
|
|
2613
|
+
gracefulTimer.unref?.();
|
|
2614
|
+
})
|
|
2576
2615
|
]);
|
|
2616
|
+
if (gracefulTimer) clearTimeout(gracefulTimer);
|
|
2577
2617
|
if (gracefulRace === "timeout") {
|
|
2578
2618
|
forceKillTree(child);
|
|
2619
|
+
let forceTimer;
|
|
2579
2620
|
await Promise.race([
|
|
2580
2621
|
exitPromise,
|
|
2581
|
-
new Promise((resolve) =>
|
|
2622
|
+
new Promise((resolve) => {
|
|
2623
|
+
forceTimer = setTimeout(resolve, FORCE_TIMEOUT_MS);
|
|
2624
|
+
forceTimer.unref?.();
|
|
2625
|
+
})
|
|
2582
2626
|
]);
|
|
2627
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
2583
2628
|
}
|
|
2584
2629
|
child.stdout?.removeAllListeners();
|
|
2585
2630
|
child.stderr?.removeAllListeners();
|
|
@@ -2709,49 +2754,53 @@ var MCPClient = class _MCPClient {
|
|
|
2709
2754
|
this.pending.clear();
|
|
2710
2755
|
}
|
|
2711
2756
|
async notify(method, params) {
|
|
2757
|
+
if (this._drainPending) {
|
|
2758
|
+
this._lastNotifySkipped = true;
|
|
2759
|
+
console.warn(
|
|
2760
|
+
JSON.stringify({
|
|
2761
|
+
level: "warn",
|
|
2762
|
+
event: "mcp.notify_skipped_backpressure",
|
|
2763
|
+
server: this.opts.name,
|
|
2764
|
+
method,
|
|
2765
|
+
message: "stdin buffer backpressure (already waiting for drain)",
|
|
2766
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2767
|
+
})
|
|
2768
|
+
);
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
const stdin = this.child?.stdin;
|
|
2772
|
+
if (!stdin || stdin.destroyed === true || stdin.writable === false) {
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2712
2775
|
const req = { jsonrpc: "2.0", method, params };
|
|
2713
2776
|
const encoded = JSON.stringify(req) + "\n";
|
|
2714
2777
|
try {
|
|
2715
|
-
const ok =
|
|
2778
|
+
const ok = stdin.write(encoded);
|
|
2716
2779
|
if (!ok) {
|
|
2717
|
-
if (this._drainPending) {
|
|
2718
|
-
this._lastNotifySkipped = true;
|
|
2719
|
-
console.warn(
|
|
2720
|
-
JSON.stringify({
|
|
2721
|
-
level: "warn",
|
|
2722
|
-
event: "mcp.notify_skipped_backpressure",
|
|
2723
|
-
server: this.opts.name,
|
|
2724
|
-
method,
|
|
2725
|
-
message: "stdin buffer backpressure (already waiting for drain)",
|
|
2726
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2727
|
-
})
|
|
2728
|
-
);
|
|
2729
|
-
return;
|
|
2730
|
-
}
|
|
2731
2780
|
this._drainPending = true;
|
|
2732
2781
|
await new Promise((resolve, reject) => {
|
|
2733
2782
|
const timeout = setTimeout(() => {
|
|
2734
|
-
|
|
2735
|
-
|
|
2783
|
+
stdin.removeListener?.("drain", onDrain);
|
|
2784
|
+
stdin.removeListener?.("error", onError);
|
|
2736
2785
|
this._drainPending = false;
|
|
2737
2786
|
reject(new Error(`MCP notify("${method}") drain timeout`));
|
|
2738
2787
|
}, 500);
|
|
2739
2788
|
const onDrain = () => {
|
|
2740
2789
|
clearTimeout(timeout);
|
|
2741
|
-
|
|
2742
|
-
|
|
2790
|
+
stdin.removeListener?.("drain", onDrain);
|
|
2791
|
+
stdin.removeListener?.("error", onError);
|
|
2743
2792
|
this._drainPending = false;
|
|
2744
2793
|
resolve();
|
|
2745
2794
|
};
|
|
2746
2795
|
const onError = (err) => {
|
|
2747
2796
|
clearTimeout(timeout);
|
|
2748
|
-
|
|
2749
|
-
|
|
2797
|
+
stdin.removeListener?.("drain", onDrain);
|
|
2798
|
+
stdin.removeListener?.("error", onError);
|
|
2750
2799
|
this._drainPending = false;
|
|
2751
2800
|
reject(err);
|
|
2752
2801
|
};
|
|
2753
|
-
|
|
2754
|
-
|
|
2802
|
+
stdin.once?.("drain", onDrain);
|
|
2803
|
+
stdin.once?.("error", onError);
|
|
2755
2804
|
});
|
|
2756
2805
|
}
|
|
2757
2806
|
} catch (err) {
|
|
@@ -2769,12 +2818,16 @@ var MCPClient = class _MCPClient {
|
|
|
2769
2818
|
void this.close();
|
|
2770
2819
|
return;
|
|
2771
2820
|
}
|
|
2821
|
+
let start = 0;
|
|
2772
2822
|
let idx = this.rxBuffer.indexOf("\n");
|
|
2773
2823
|
while (idx !== -1) {
|
|
2774
|
-
const line = this.rxBuffer.slice(
|
|
2775
|
-
|
|
2824
|
+
const line = this.rxBuffer.slice(start, idx).trim();
|
|
2825
|
+
start = idx + 1;
|
|
2776
2826
|
if (line) this.onLine(line);
|
|
2777
|
-
idx = this.rxBuffer.indexOf("\n");
|
|
2827
|
+
idx = this.rxBuffer.indexOf("\n", start);
|
|
2828
|
+
}
|
|
2829
|
+
if (start > 0) {
|
|
2830
|
+
this.rxBuffer = this.rxBuffer.slice(start);
|
|
2778
2831
|
}
|
|
2779
2832
|
}
|
|
2780
2833
|
onLine(line) {
|
|
@@ -3040,11 +3093,7 @@ async function persist(configPath, full, servers) {
|
|
|
3040
3093
|
full.mcpServers = servers;
|
|
3041
3094
|
await writeConfig(configPath, full);
|
|
3042
3095
|
}
|
|
3043
|
-
var UNSAFE_SERVER_NAMES = /* @__PURE__ */ new Set([
|
|
3044
|
-
"__proto__",
|
|
3045
|
-
"constructor",
|
|
3046
|
-
"prototype"
|
|
3047
|
-
]);
|
|
3096
|
+
var UNSAFE_SERVER_NAMES = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
3048
3097
|
function unsafeServerNameResult(name) {
|
|
3049
3098
|
if (!UNSAFE_SERVER_NAMES.has(name)) return void 0;
|
|
3050
3099
|
return { ok: false, message: `Invalid server name "${name}"` };
|
|
@@ -3615,7 +3664,8 @@ function wrapMCPTool(serverName, mcpTool, client, permission = "confirm", observ
|
|
|
3615
3664
|
const signal = opts?.signal ?? ctx?.signal;
|
|
3616
3665
|
const res = await live.callTool(mcpTool.name, input, signal ? { signal } : void 0);
|
|
3617
3666
|
if (res.isError) {
|
|
3618
|
-
|
|
3667
|
+
const errText = stringify(res.content);
|
|
3668
|
+
throw new Error(errText || `MCP tool "${qualifiedName}" failed`);
|
|
3619
3669
|
}
|
|
3620
3670
|
ok = true;
|
|
3621
3671
|
return stringify(res.content);
|
|
@@ -3664,6 +3714,7 @@ function applySlotTools(ctx, slot, tools, client) {
|
|
|
3664
3714
|
},
|
|
3665
3715
|
onFinish: ({ durationMs, ok }) => {
|
|
3666
3716
|
slot.operations.inFlightCalls = Math.max(0, slot.operations.inFlightCalls - 1);
|
|
3717
|
+
slot.lastUsed = Date.now();
|
|
3667
3718
|
pushBounded(slot.operations.callSamples, durationMs, MCP_OPERATION_LIMITS.LATENCY_SAMPLES);
|
|
3668
3719
|
if (ok) {
|
|
3669
3720
|
ctx.recordSuccess(slot);
|
|
@@ -3787,7 +3838,7 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
3787
3838
|
client.addToolsChangedListener(ctx.onToolsChanged);
|
|
3788
3839
|
ctx.addCatalogListeners(client);
|
|
3789
3840
|
await client.connect();
|
|
3790
|
-
if (slot.state === "disconnected" || ctx.servers.has(slot.cfg.name)
|
|
3841
|
+
if (slot.state === "disconnected" || !ctx.servers.has(slot.cfg.name) || ctx.servers.get(slot.cfg.name) !== slot) {
|
|
3791
3842
|
client.removeExitListener(ctx.onChildExit);
|
|
3792
3843
|
if (boundDisconnect) client.removeDisconnectListener(boundDisconnect);
|
|
3793
3844
|
client.removeToolsChangedListener(ctx.onToolsChanged);
|
|
@@ -3890,8 +3941,28 @@ function resetDisconnectedSlotTools(slot, toolRegistry) {
|
|
|
3890
3941
|
slot.resourceTemplates = void 0;
|
|
3891
3942
|
slot.prompts = void 0;
|
|
3892
3943
|
}
|
|
3893
|
-
function markLazySlotDormant(slot, events, reason) {
|
|
3894
|
-
slot.
|
|
3944
|
+
function markLazySlotDormant(slot, events, reason, options) {
|
|
3945
|
+
slot.reconnectPending = false;
|
|
3946
|
+
if (slot.reconnectTimer) {
|
|
3947
|
+
clearTimeout(slot.reconnectTimer);
|
|
3948
|
+
slot.reconnectTimer = void 0;
|
|
3949
|
+
}
|
|
3950
|
+
if (slot.client) {
|
|
3951
|
+
if (options?.onChildExit) {
|
|
3952
|
+
slot.client.removeExitListener?.(options.onChildExit);
|
|
3953
|
+
}
|
|
3954
|
+
if (slot.onDisconnect) {
|
|
3955
|
+
slot.client.removeDisconnectListener?.(slot.onDisconnect);
|
|
3956
|
+
}
|
|
3957
|
+
if (options?.onToolsChanged) {
|
|
3958
|
+
slot.client.removeToolsChangedListener?.(options.onToolsChanged);
|
|
3959
|
+
}
|
|
3960
|
+
options?.removeCatalogListeners?.(slot.client);
|
|
3961
|
+
slot.client.close?.().catch(() => {
|
|
3962
|
+
});
|
|
3963
|
+
slot.client = void 0;
|
|
3964
|
+
}
|
|
3965
|
+
slot.onDisconnect = void 0;
|
|
3895
3966
|
slot.state = "dormant";
|
|
3896
3967
|
events.emit("mcp.server.disconnected", {
|
|
3897
3968
|
name: slot.cfg.name,
|
|
@@ -3954,17 +4025,23 @@ function buildRegistryOperationalHealth(servers, disabledServers) {
|
|
|
3954
4025
|
|
|
3955
4026
|
// src/registry-idle.ts
|
|
3956
4027
|
async function sleepIdleSlot(ctx, slot) {
|
|
4028
|
+
if (slot.operations.inFlightCalls > 0) return;
|
|
3957
4029
|
slot.reconnectPending = false;
|
|
3958
4030
|
if (slot.reconnectTimer) {
|
|
3959
4031
|
clearTimeout(slot.reconnectTimer);
|
|
3960
4032
|
slot.reconnectTimer = void 0;
|
|
3961
4033
|
}
|
|
3962
4034
|
if (slot.client) {
|
|
3963
|
-
slot.client
|
|
3964
|
-
|
|
3965
|
-
slot.client.
|
|
3966
|
-
|
|
3967
|
-
|
|
4035
|
+
const client = slot.client;
|
|
4036
|
+
client.removeExitListener?.(ctx.onChildExit);
|
|
4037
|
+
if (slot.onDisconnect) client.removeDisconnectListener?.(slot.onDisconnect);
|
|
4038
|
+
client.removeToolsChangedListener?.(ctx.onToolsChanged);
|
|
4039
|
+
ctx.removeCatalogListeners(client);
|
|
4040
|
+
try {
|
|
4041
|
+
await client.close?.();
|
|
4042
|
+
} catch (err) {
|
|
4043
|
+
ctx.log.warn(`MCP server "${slot.cfg.name}" error during idle sleep close`, err);
|
|
4044
|
+
}
|
|
3968
4045
|
slot.client = void 0;
|
|
3969
4046
|
}
|
|
3970
4047
|
slot.onDisconnect = void 0;
|
|
@@ -3978,7 +4055,7 @@ async function sweepIdleSlots(ctx) {
|
|
|
3978
4055
|
if (ctx.idleTimeoutMs <= 0) return false;
|
|
3979
4056
|
const now = Date.now();
|
|
3980
4057
|
for (const slot of ctx.servers.values()) {
|
|
3981
|
-
if (slot.lazy && slot.state === "connected" && slot.client && now - slot.lastUsed > ctx.idleTimeoutMs) {
|
|
4058
|
+
if (slot.lazy && slot.state === "connected" && slot.client && slot.operations.inFlightCalls === 0 && now - slot.lastUsed > ctx.idleTimeoutMs) {
|
|
3982
4059
|
await sleepIdleSlot(ctx, slot);
|
|
3983
4060
|
}
|
|
3984
4061
|
}
|
|
@@ -4066,6 +4143,7 @@ function scheduleRegistryReconnect({
|
|
|
4066
4143
|
slot.reconnectTimer = void 0;
|
|
4067
4144
|
void attemptReconnect(slot);
|
|
4068
4145
|
}, delay);
|
|
4146
|
+
slot.reconnectTimer.unref?.();
|
|
4069
4147
|
}
|
|
4070
4148
|
|
|
4071
4149
|
// src/registry.ts
|
|
@@ -4213,7 +4291,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4213
4291
|
if (!slot) throw new Error(`MCP server "${name}" not registered`);
|
|
4214
4292
|
slot.lastUsed = Date.now();
|
|
4215
4293
|
if (slot.client && slot.state === "connected") return slot.client;
|
|
4216
|
-
const waking = slot.state === "dormant";
|
|
4294
|
+
const waking = slot.state === "dormant" && !slot.connecting;
|
|
4217
4295
|
if (waking) {
|
|
4218
4296
|
slot.operations.wakeCount++;
|
|
4219
4297
|
this.recordOperation(slot, "wake", "lazy-demand");
|
|
@@ -4289,22 +4367,21 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4289
4367
|
}
|
|
4290
4368
|
slot.state = "disconnected";
|
|
4291
4369
|
if (slot.client) {
|
|
4292
|
-
slot.client
|
|
4293
|
-
|
|
4294
|
-
slot.client.
|
|
4295
|
-
|
|
4296
|
-
|
|
4370
|
+
const client = slot.client;
|
|
4371
|
+
client.removeExitListener?.(this.onChildExit);
|
|
4372
|
+
if (slot.onDisconnect) client.removeDisconnectListener?.(slot.onDisconnect);
|
|
4373
|
+
client.removeToolsChangedListener?.(this.onToolsChanged);
|
|
4374
|
+
this.removeCatalogListeners(client);
|
|
4375
|
+
try {
|
|
4376
|
+
await client.close?.();
|
|
4377
|
+
} catch (err) {
|
|
4378
|
+
this.log.warn(`MCP server "${name}" error during stop close`, err);
|
|
4379
|
+
}
|
|
4297
4380
|
slot.client = void 0;
|
|
4298
4381
|
}
|
|
4299
4382
|
slot.onDisconnect = void 0;
|
|
4300
4383
|
slot.connecting = void 0;
|
|
4301
|
-
|
|
4302
|
-
slot.toolNames = [];
|
|
4303
|
-
slot.lazyTools = [];
|
|
4304
|
-
slot.serverMetadata = void 0;
|
|
4305
|
-
slot.resources = void 0;
|
|
4306
|
-
slot.resourceTemplates = void 0;
|
|
4307
|
-
slot.prompts = void 0;
|
|
4384
|
+
resetDisconnectedSlotTools(slot, this.toolRegistry);
|
|
4308
4385
|
slot.registeredLazy = false;
|
|
4309
4386
|
this.recordOperation(slot, "stop", "manual");
|
|
4310
4387
|
this.events.emit("mcp.server.disconnected", { name, reason: "stop" });
|
|
@@ -4348,8 +4425,17 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4348
4425
|
}
|
|
4349
4426
|
getCatalog(name) {
|
|
4350
4427
|
const slot = this.servers.get(name);
|
|
4351
|
-
if (
|
|
4352
|
-
|
|
4428
|
+
if (slot) {
|
|
4429
|
+
return registryCatalogSnapshot(slot);
|
|
4430
|
+
}
|
|
4431
|
+
const disabled = this.disabledServers.get(name);
|
|
4432
|
+
if (disabled) {
|
|
4433
|
+
return {
|
|
4434
|
+
name: disabled.name,
|
|
4435
|
+
state: "idle"
|
|
4436
|
+
};
|
|
4437
|
+
}
|
|
4438
|
+
return void 0;
|
|
4353
4439
|
}
|
|
4354
4440
|
async listResources(name, opts = {}) {
|
|
4355
4441
|
const slot = this.requireSlot(name);
|
|
@@ -4506,9 +4592,16 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4506
4592
|
clearInterval(this.idleTimer);
|
|
4507
4593
|
this.idleTimer = void 0;
|
|
4508
4594
|
}
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4595
|
+
const names = Array.from(this.servers.keys());
|
|
4596
|
+
await Promise.all(
|
|
4597
|
+
names.map(async (name) => {
|
|
4598
|
+
try {
|
|
4599
|
+
await this.stop(name);
|
|
4600
|
+
} catch (err) {
|
|
4601
|
+
this.log.warn(`MCP server "${name}" failed to stop during stopAll`, err);
|
|
4602
|
+
}
|
|
4603
|
+
})
|
|
4604
|
+
);
|
|
4512
4605
|
this.disabledServers.clear();
|
|
4513
4606
|
}
|
|
4514
4607
|
/**
|
|
@@ -4570,15 +4663,19 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4570
4663
|
client.addPromptsChangedListener(this.onPromptsChanged);
|
|
4571
4664
|
}
|
|
4572
4665
|
removeCatalogListeners(client) {
|
|
4573
|
-
client.removeResourcesChangedListener(this.onResourcesChanged);
|
|
4574
|
-
client.removePromptsChangedListener(this.onPromptsChanged);
|
|
4666
|
+
client.removeResourcesChangedListener?.(this.onResourcesChanged);
|
|
4667
|
+
client.removePromptsChangedListener?.(this.onPromptsChanged);
|
|
4575
4668
|
}
|
|
4576
4669
|
onChildExit = (name, code, _signal) => {
|
|
4577
4670
|
const slot = this.servers.get(name);
|
|
4578
4671
|
if (!slot) return;
|
|
4579
4672
|
if (slot.lazy) {
|
|
4580
4673
|
this.recordFailure(slot, "transport", "process-exit-lazy");
|
|
4581
|
-
markLazySlotDormant(slot, this.events, `exit:${code ?? "unknown"}
|
|
4674
|
+
markLazySlotDormant(slot, this.events, `exit:${code ?? "unknown"}`, {
|
|
4675
|
+
onChildExit: this.onChildExit,
|
|
4676
|
+
onToolsChanged: this.onToolsChanged,
|
|
4677
|
+
removeCatalogListeners: (c) => this.removeCatalogListeners(c)
|
|
4678
|
+
});
|
|
4582
4679
|
return;
|
|
4583
4680
|
}
|
|
4584
4681
|
resetDisconnectedSlotTools(slot, this.toolRegistry);
|
|
@@ -4593,7 +4690,11 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4593
4690
|
if (!slot) return;
|
|
4594
4691
|
if (slot.lazy) {
|
|
4595
4692
|
this.recordFailure(slot, "transport", "http-disconnect-lazy");
|
|
4596
|
-
markLazySlotDormant(slot, this.events, "http-disconnect"
|
|
4693
|
+
markLazySlotDormant(slot, this.events, "http-disconnect", {
|
|
4694
|
+
onChildExit: this.onChildExit,
|
|
4695
|
+
onToolsChanged: this.onToolsChanged,
|
|
4696
|
+
removeCatalogListeners: (c) => this.removeCatalogListeners(c)
|
|
4697
|
+
});
|
|
4597
4698
|
return;
|
|
4598
4699
|
}
|
|
4599
4700
|
resetDisconnectedSlotTools(slot, this.toolRegistry);
|
|
@@ -4956,11 +5057,14 @@ function serveStdio(server, opts = {}) {
|
|
|
4956
5057
|
onEnd();
|
|
4957
5058
|
return;
|
|
4958
5059
|
}
|
|
4959
|
-
let
|
|
5060
|
+
let start = 0;
|
|
5061
|
+
let idx = buffer.indexOf("\n", start);
|
|
4960
5062
|
while (idx !== -1) {
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
5063
|
+
let end = idx;
|
|
5064
|
+
if (end > start && buffer.charCodeAt(end - 1) === 13) end--;
|
|
5065
|
+
const line = buffer.slice(start, end);
|
|
5066
|
+
start = idx + 1;
|
|
5067
|
+
idx = buffer.indexOf("\n", start);
|
|
4964
5068
|
if (!line.trim()) continue;
|
|
4965
5069
|
const handler = server.handleMessage(line).then((res) => {
|
|
4966
5070
|
if (res !== null) writeLine(res);
|
|
@@ -4978,6 +5082,7 @@ function serveStdio(server, opts = {}) {
|
|
|
4978
5082
|
});
|
|
4979
5083
|
inFlightHandlers.add(handler);
|
|
4980
5084
|
}
|
|
5085
|
+
if (start > 0) buffer = buffer.slice(start);
|
|
4981
5086
|
};
|
|
4982
5087
|
let resolveDone;
|
|
4983
5088
|
const done = new Promise((resolve) => {
|
|
@@ -4989,6 +5094,25 @@ function serveStdio(server, opts = {}) {
|
|
|
4989
5094
|
if (closed) return;
|
|
4990
5095
|
closed = true;
|
|
4991
5096
|
stdin.off("data", onData);
|
|
5097
|
+
if (!bufferTooLarge && buffer.trim()) {
|
|
5098
|
+
const line = buffer.trim();
|
|
5099
|
+
buffer = "";
|
|
5100
|
+
const handler = server.handleMessage(line).then((res) => {
|
|
5101
|
+
if (res !== null) writeLine(res);
|
|
5102
|
+
}).catch((err) => {
|
|
5103
|
+
console.error(
|
|
5104
|
+
JSON.stringify({
|
|
5105
|
+
level: "error",
|
|
5106
|
+
event: "mcp_server.handle_message_failed",
|
|
5107
|
+
message: toErrorMessage2(err),
|
|
5108
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5109
|
+
})
|
|
5110
|
+
);
|
|
5111
|
+
}).finally(() => {
|
|
5112
|
+
inFlightHandlers.delete(handler);
|
|
5113
|
+
});
|
|
5114
|
+
inFlightHandlers.add(handler);
|
|
5115
|
+
}
|
|
4992
5116
|
resolveDone();
|
|
4993
5117
|
};
|
|
4994
5118
|
stdin.on("data", onData);
|
|
@@ -5073,18 +5197,24 @@ async function handleHttpRequest(server, req, res, token, log, boundHost) {
|
|
|
5073
5197
|
return send(415, JSON.stringify({ error: "content-type must be application/json" }));
|
|
5074
5198
|
}
|
|
5075
5199
|
let body = "";
|
|
5200
|
+
let aborted = false;
|
|
5076
5201
|
req.on("data", (chunk) => {
|
|
5202
|
+
if (aborted) return;
|
|
5077
5203
|
body += chunk.toString("utf8");
|
|
5078
5204
|
if (body.length > HTTP_BODY_CAP) {
|
|
5205
|
+
aborted = true;
|
|
5079
5206
|
send(413, JSON.stringify({ error: "payload too large" }));
|
|
5080
5207
|
req.destroy();
|
|
5081
5208
|
}
|
|
5082
5209
|
});
|
|
5083
5210
|
req.on("end", () => {
|
|
5211
|
+
if (aborted) return;
|
|
5084
5212
|
void server.handleMessage(body).then((out) => {
|
|
5213
|
+
if (aborted) return;
|
|
5085
5214
|
if (out === null) return send(202, "");
|
|
5086
5215
|
return send(200, out);
|
|
5087
5216
|
}).catch((err) => {
|
|
5217
|
+
if (aborted) return;
|
|
5088
5218
|
log?.warn?.(`MCP http handler error: ${toErrorMessage2(err)}`);
|
|
5089
5219
|
send(500, JSON.stringify({ error: "internal error" }));
|
|
5090
5220
|
});
|
|
@@ -2,5 +2,12 @@ import type { EventBus } from '@wrongstack/core/kernel';
|
|
|
2
2
|
import type { ToolRegistry } from '@wrongstack/core/registry';
|
|
3
3
|
import type { ServerSlot } from './registry-slots.js';
|
|
4
4
|
export declare function resetDisconnectedSlotTools(slot: ServerSlot, toolRegistry: ToolRegistry): void;
|
|
5
|
-
export
|
|
5
|
+
export interface MarkLazySlotDormantOptions {
|
|
6
|
+
onChildExit?: (name: string, code: number | null, signal: string | null) => void;
|
|
7
|
+
onToolsChanged?: (name: string, tools: {
|
|
8
|
+
name: string;
|
|
9
|
+
}[]) => void;
|
|
10
|
+
removeCatalogListeners?: (client: import('./client.js').MCPClient) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function markLazySlotDormant(slot: ServerSlot, events: EventBus, reason: string, options?: MarkLazySlotDormantOptions): void;
|
|
6
13
|
//# sourceMappingURL=registry-disconnect.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.320.1",
|
|
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": "0.
|
|
29
|
+
"@wrongstack/core": "0.320.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^26.2.0",
|