@wrongstack/mcp 0.319.1 → 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 +214 -80
- 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) {
|
|
@@ -3611,7 +3664,8 @@ function wrapMCPTool(serverName, mcpTool, client, permission = "confirm", observ
|
|
|
3611
3664
|
const signal = opts?.signal ?? ctx?.signal;
|
|
3612
3665
|
const res = await live.callTool(mcpTool.name, input, signal ? { signal } : void 0);
|
|
3613
3666
|
if (res.isError) {
|
|
3614
|
-
|
|
3667
|
+
const errText = stringify(res.content);
|
|
3668
|
+
throw new Error(errText || `MCP tool "${qualifiedName}" failed`);
|
|
3615
3669
|
}
|
|
3616
3670
|
ok = true;
|
|
3617
3671
|
return stringify(res.content);
|
|
@@ -3660,6 +3714,7 @@ function applySlotTools(ctx, slot, tools, client) {
|
|
|
3660
3714
|
},
|
|
3661
3715
|
onFinish: ({ durationMs, ok }) => {
|
|
3662
3716
|
slot.operations.inFlightCalls = Math.max(0, slot.operations.inFlightCalls - 1);
|
|
3717
|
+
slot.lastUsed = Date.now();
|
|
3663
3718
|
pushBounded(slot.operations.callSamples, durationMs, MCP_OPERATION_LIMITS.LATENCY_SAMPLES);
|
|
3664
3719
|
if (ok) {
|
|
3665
3720
|
ctx.recordSuccess(slot);
|
|
@@ -3783,7 +3838,7 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
3783
3838
|
client.addToolsChangedListener(ctx.onToolsChanged);
|
|
3784
3839
|
ctx.addCatalogListeners(client);
|
|
3785
3840
|
await client.connect();
|
|
3786
|
-
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) {
|
|
3787
3842
|
client.removeExitListener(ctx.onChildExit);
|
|
3788
3843
|
if (boundDisconnect) client.removeDisconnectListener(boundDisconnect);
|
|
3789
3844
|
client.removeToolsChangedListener(ctx.onToolsChanged);
|
|
@@ -3886,8 +3941,28 @@ function resetDisconnectedSlotTools(slot, toolRegistry) {
|
|
|
3886
3941
|
slot.resourceTemplates = void 0;
|
|
3887
3942
|
slot.prompts = void 0;
|
|
3888
3943
|
}
|
|
3889
|
-
function markLazySlotDormant(slot, events, reason) {
|
|
3890
|
-
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;
|
|
3891
3966
|
slot.state = "dormant";
|
|
3892
3967
|
events.emit("mcp.server.disconnected", {
|
|
3893
3968
|
name: slot.cfg.name,
|
|
@@ -3950,17 +4025,23 @@ function buildRegistryOperationalHealth(servers, disabledServers) {
|
|
|
3950
4025
|
|
|
3951
4026
|
// src/registry-idle.ts
|
|
3952
4027
|
async function sleepIdleSlot(ctx, slot) {
|
|
4028
|
+
if (slot.operations.inFlightCalls > 0) return;
|
|
3953
4029
|
slot.reconnectPending = false;
|
|
3954
4030
|
if (slot.reconnectTimer) {
|
|
3955
4031
|
clearTimeout(slot.reconnectTimer);
|
|
3956
4032
|
slot.reconnectTimer = void 0;
|
|
3957
4033
|
}
|
|
3958
4034
|
if (slot.client) {
|
|
3959
|
-
slot.client
|
|
3960
|
-
|
|
3961
|
-
slot.client.
|
|
3962
|
-
|
|
3963
|
-
|
|
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
|
+
}
|
|
3964
4045
|
slot.client = void 0;
|
|
3965
4046
|
}
|
|
3966
4047
|
slot.onDisconnect = void 0;
|
|
@@ -3974,7 +4055,7 @@ async function sweepIdleSlots(ctx) {
|
|
|
3974
4055
|
if (ctx.idleTimeoutMs <= 0) return false;
|
|
3975
4056
|
const now = Date.now();
|
|
3976
4057
|
for (const slot of ctx.servers.values()) {
|
|
3977
|
-
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) {
|
|
3978
4059
|
await sleepIdleSlot(ctx, slot);
|
|
3979
4060
|
}
|
|
3980
4061
|
}
|
|
@@ -4062,6 +4143,7 @@ function scheduleRegistryReconnect({
|
|
|
4062
4143
|
slot.reconnectTimer = void 0;
|
|
4063
4144
|
void attemptReconnect(slot);
|
|
4064
4145
|
}, delay);
|
|
4146
|
+
slot.reconnectTimer.unref?.();
|
|
4065
4147
|
}
|
|
4066
4148
|
|
|
4067
4149
|
// src/registry.ts
|
|
@@ -4209,7 +4291,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4209
4291
|
if (!slot) throw new Error(`MCP server "${name}" not registered`);
|
|
4210
4292
|
slot.lastUsed = Date.now();
|
|
4211
4293
|
if (slot.client && slot.state === "connected") return slot.client;
|
|
4212
|
-
const waking = slot.state === "dormant";
|
|
4294
|
+
const waking = slot.state === "dormant" && !slot.connecting;
|
|
4213
4295
|
if (waking) {
|
|
4214
4296
|
slot.operations.wakeCount++;
|
|
4215
4297
|
this.recordOperation(slot, "wake", "lazy-demand");
|
|
@@ -4285,22 +4367,21 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4285
4367
|
}
|
|
4286
4368
|
slot.state = "disconnected";
|
|
4287
4369
|
if (slot.client) {
|
|
4288
|
-
slot.client
|
|
4289
|
-
|
|
4290
|
-
slot.client.
|
|
4291
|
-
|
|
4292
|
-
|
|
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
|
+
}
|
|
4293
4380
|
slot.client = void 0;
|
|
4294
4381
|
}
|
|
4295
4382
|
slot.onDisconnect = void 0;
|
|
4296
4383
|
slot.connecting = void 0;
|
|
4297
|
-
|
|
4298
|
-
slot.toolNames = [];
|
|
4299
|
-
slot.lazyTools = [];
|
|
4300
|
-
slot.serverMetadata = void 0;
|
|
4301
|
-
slot.resources = void 0;
|
|
4302
|
-
slot.resourceTemplates = void 0;
|
|
4303
|
-
slot.prompts = void 0;
|
|
4384
|
+
resetDisconnectedSlotTools(slot, this.toolRegistry);
|
|
4304
4385
|
slot.registeredLazy = false;
|
|
4305
4386
|
this.recordOperation(slot, "stop", "manual");
|
|
4306
4387
|
this.events.emit("mcp.server.disconnected", { name, reason: "stop" });
|
|
@@ -4344,8 +4425,17 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4344
4425
|
}
|
|
4345
4426
|
getCatalog(name) {
|
|
4346
4427
|
const slot = this.servers.get(name);
|
|
4347
|
-
if (
|
|
4348
|
-
|
|
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;
|
|
4349
4439
|
}
|
|
4350
4440
|
async listResources(name, opts = {}) {
|
|
4351
4441
|
const slot = this.requireSlot(name);
|
|
@@ -4502,9 +4592,16 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4502
4592
|
clearInterval(this.idleTimer);
|
|
4503
4593
|
this.idleTimer = void 0;
|
|
4504
4594
|
}
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
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
|
+
);
|
|
4508
4605
|
this.disabledServers.clear();
|
|
4509
4606
|
}
|
|
4510
4607
|
/**
|
|
@@ -4566,15 +4663,19 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4566
4663
|
client.addPromptsChangedListener(this.onPromptsChanged);
|
|
4567
4664
|
}
|
|
4568
4665
|
removeCatalogListeners(client) {
|
|
4569
|
-
client.removeResourcesChangedListener(this.onResourcesChanged);
|
|
4570
|
-
client.removePromptsChangedListener(this.onPromptsChanged);
|
|
4666
|
+
client.removeResourcesChangedListener?.(this.onResourcesChanged);
|
|
4667
|
+
client.removePromptsChangedListener?.(this.onPromptsChanged);
|
|
4571
4668
|
}
|
|
4572
4669
|
onChildExit = (name, code, _signal) => {
|
|
4573
4670
|
const slot = this.servers.get(name);
|
|
4574
4671
|
if (!slot) return;
|
|
4575
4672
|
if (slot.lazy) {
|
|
4576
4673
|
this.recordFailure(slot, "transport", "process-exit-lazy");
|
|
4577
|
-
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
|
+
});
|
|
4578
4679
|
return;
|
|
4579
4680
|
}
|
|
4580
4681
|
resetDisconnectedSlotTools(slot, this.toolRegistry);
|
|
@@ -4589,7 +4690,11 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4589
4690
|
if (!slot) return;
|
|
4590
4691
|
if (slot.lazy) {
|
|
4591
4692
|
this.recordFailure(slot, "transport", "http-disconnect-lazy");
|
|
4592
|
-
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
|
+
});
|
|
4593
4698
|
return;
|
|
4594
4699
|
}
|
|
4595
4700
|
resetDisconnectedSlotTools(slot, this.toolRegistry);
|
|
@@ -4952,11 +5057,14 @@ function serveStdio(server, opts = {}) {
|
|
|
4952
5057
|
onEnd();
|
|
4953
5058
|
return;
|
|
4954
5059
|
}
|
|
4955
|
-
let
|
|
5060
|
+
let start = 0;
|
|
5061
|
+
let idx = buffer.indexOf("\n", start);
|
|
4956
5062
|
while (idx !== -1) {
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
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);
|
|
4960
5068
|
if (!line.trim()) continue;
|
|
4961
5069
|
const handler = server.handleMessage(line).then((res) => {
|
|
4962
5070
|
if (res !== null) writeLine(res);
|
|
@@ -4974,6 +5082,7 @@ function serveStdio(server, opts = {}) {
|
|
|
4974
5082
|
});
|
|
4975
5083
|
inFlightHandlers.add(handler);
|
|
4976
5084
|
}
|
|
5085
|
+
if (start > 0) buffer = buffer.slice(start);
|
|
4977
5086
|
};
|
|
4978
5087
|
let resolveDone;
|
|
4979
5088
|
const done = new Promise((resolve) => {
|
|
@@ -4985,6 +5094,25 @@ function serveStdio(server, opts = {}) {
|
|
|
4985
5094
|
if (closed) return;
|
|
4986
5095
|
closed = true;
|
|
4987
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
|
+
}
|
|
4988
5116
|
resolveDone();
|
|
4989
5117
|
};
|
|
4990
5118
|
stdin.on("data", onData);
|
|
@@ -5069,18 +5197,24 @@ async function handleHttpRequest(server, req, res, token, log, boundHost) {
|
|
|
5069
5197
|
return send(415, JSON.stringify({ error: "content-type must be application/json" }));
|
|
5070
5198
|
}
|
|
5071
5199
|
let body = "";
|
|
5200
|
+
let aborted = false;
|
|
5072
5201
|
req.on("data", (chunk) => {
|
|
5202
|
+
if (aborted) return;
|
|
5073
5203
|
body += chunk.toString("utf8");
|
|
5074
5204
|
if (body.length > HTTP_BODY_CAP) {
|
|
5205
|
+
aborted = true;
|
|
5075
5206
|
send(413, JSON.stringify({ error: "payload too large" }));
|
|
5076
5207
|
req.destroy();
|
|
5077
5208
|
}
|
|
5078
5209
|
});
|
|
5079
5210
|
req.on("end", () => {
|
|
5211
|
+
if (aborted) return;
|
|
5080
5212
|
void server.handleMessage(body).then((out) => {
|
|
5213
|
+
if (aborted) return;
|
|
5081
5214
|
if (out === null) return send(202, "");
|
|
5082
5215
|
return send(200, out);
|
|
5083
5216
|
}).catch((err) => {
|
|
5217
|
+
if (aborted) return;
|
|
5084
5218
|
log?.warn?.(`MCP http handler error: ${toErrorMessage2(err)}`);
|
|
5085
5219
|
send(500, JSON.stringify({ error: "internal error" }));
|
|
5086
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",
|