@wrongstack/mcp 0.306.4 → 0.307.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/dist/index.js +315 -275
- package/dist/registry-connect-loop.d.ts +34 -0
- package/dist/registry-idle.d.ts +20 -0
- package/dist/registry.d.ts +2 -15
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -3469,7 +3469,7 @@ function percentile(sorted, ratio) {
|
|
|
3469
3469
|
}
|
|
3470
3470
|
|
|
3471
3471
|
// src/registry.ts
|
|
3472
|
-
import { expectDefined } from "@wrongstack/core/utils";
|
|
3472
|
+
import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
|
|
3473
3473
|
|
|
3474
3474
|
// src/registry-catalog.ts
|
|
3475
3475
|
var MAX_CATALOG_PAGES = 100;
|
|
@@ -3695,6 +3695,9 @@ function scheduleRegistryReconnect({
|
|
|
3695
3695
|
}, delay);
|
|
3696
3696
|
}
|
|
3697
3697
|
|
|
3698
|
+
// src/registry-connect-loop.ts
|
|
3699
|
+
import { expectDefined } from "@wrongstack/core/utils";
|
|
3700
|
+
|
|
3698
3701
|
// src/wrap-tool.ts
|
|
3699
3702
|
import { ToolCapabilities } from "@wrongstack/core/security";
|
|
3700
3703
|
import { mcpQualifiedToolName } from "@wrongstack/core/utils";
|
|
@@ -3761,6 +3764,274 @@ function stringify(c) {
|
|
|
3761
3764
|
return String(c ?? "");
|
|
3762
3765
|
}
|
|
3763
3766
|
|
|
3767
|
+
// src/registry-connect-loop.ts
|
|
3768
|
+
function applySlotTools(ctx, slot, tools, client) {
|
|
3769
|
+
if (slot.lazy && slot.registeredLazy && !ctx.lazyMode) return;
|
|
3770
|
+
const allowed = slot.cfg.allowedTools;
|
|
3771
|
+
const filtered = tools.filter((t) => !allowed || allowed.includes(t.name));
|
|
3772
|
+
const clientArg = slot.lazy ? () => ctx.ensureConnected(slot.cfg.name) : expectDefined(client);
|
|
3773
|
+
const wrapped = filtered.map(
|
|
3774
|
+
(t) => wrapMCPTool(slot.cfg.name, t, clientArg, slot.cfg.permission ?? "confirm", {
|
|
3775
|
+
onStart: () => {
|
|
3776
|
+
slot.operations.inFlightCalls++;
|
|
3777
|
+
slot.operations.peakInFlightCalls = Math.max(
|
|
3778
|
+
slot.operations.peakInFlightCalls,
|
|
3779
|
+
slot.operations.inFlightCalls
|
|
3780
|
+
);
|
|
3781
|
+
ctx.recordOperation(slot, "call", "started", void 0, void 0, false);
|
|
3782
|
+
},
|
|
3783
|
+
onFinish: ({ durationMs, ok }) => {
|
|
3784
|
+
slot.operations.inFlightCalls = Math.max(0, slot.operations.inFlightCalls - 1);
|
|
3785
|
+
pushBounded(
|
|
3786
|
+
slot.operations.callSamples,
|
|
3787
|
+
durationMs,
|
|
3788
|
+
MCP_OPERATION_LIMITS.LATENCY_SAMPLES
|
|
3789
|
+
);
|
|
3790
|
+
if (ok) {
|
|
3791
|
+
ctx.recordSuccess(slot);
|
|
3792
|
+
ctx.recordOperation(slot, "call", "ok", void 0, durationMs, false);
|
|
3793
|
+
} else {
|
|
3794
|
+
ctx.recordFailure(slot, "tool", "tool-call-failed", durationMs);
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
})
|
|
3798
|
+
);
|
|
3799
|
+
if (ctx.lazyMode) {
|
|
3800
|
+
slot.lazyTools = wrapped;
|
|
3801
|
+
return;
|
|
3802
|
+
}
|
|
3803
|
+
for (const tool of wrapped) {
|
|
3804
|
+
try {
|
|
3805
|
+
ctx.toolRegistry.register(tool, `mcp:${slot.cfg.name}`);
|
|
3806
|
+
slot.toolNames.push(tool.name);
|
|
3807
|
+
} catch (err) {
|
|
3808
|
+
ctx.log.warn(`MCP tool "${tool.name}" not registered`, err);
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
if (slot.lazy && wrapped.length > 0) slot.registeredLazy = true;
|
|
3812
|
+
}
|
|
3813
|
+
async function discoverSlotCapabilities(ctx, slot, client) {
|
|
3814
|
+
const startedAt = Date.now();
|
|
3815
|
+
slot.serverMetadata = client.getServerMetadata();
|
|
3816
|
+
const capabilities = slot.serverMetadata?.capabilities;
|
|
3817
|
+
if (capabilities?.resources) {
|
|
3818
|
+
try {
|
|
3819
|
+
slot.resources = await collectCatalogPages(
|
|
3820
|
+
(cursor) => client.listResources(cursor ? { cursor } : {}),
|
|
3821
|
+
(page) => page.resources
|
|
3822
|
+
);
|
|
3823
|
+
} catch (err) {
|
|
3824
|
+
slot.resources = void 0;
|
|
3825
|
+
ctx.recordFailure(slot, "protocol", "resource-discovery-failed");
|
|
3826
|
+
ctx.log.warn(`MCP server "${slot.cfg.name}" resource discovery failed`, err);
|
|
3827
|
+
}
|
|
3828
|
+
try {
|
|
3829
|
+
slot.resourceTemplates = await collectCatalogPages(
|
|
3830
|
+
(cursor) => client.listResourceTemplates(cursor ? { cursor } : {}),
|
|
3831
|
+
(page) => page.resourceTemplates
|
|
3832
|
+
);
|
|
3833
|
+
} catch (err) {
|
|
3834
|
+
slot.resourceTemplates = void 0;
|
|
3835
|
+
ctx.recordFailure(slot, "protocol", "resource-template-discovery-failed");
|
|
3836
|
+
ctx.log.warn(`MCP server "${slot.cfg.name}" resource template discovery failed`, err);
|
|
3837
|
+
}
|
|
3838
|
+
} else {
|
|
3839
|
+
slot.resources = void 0;
|
|
3840
|
+
slot.resourceTemplates = void 0;
|
|
3841
|
+
}
|
|
3842
|
+
if (capabilities?.prompts) {
|
|
3843
|
+
try {
|
|
3844
|
+
slot.prompts = await collectCatalogPages(
|
|
3845
|
+
(cursor) => client.listPrompts(cursor ? { cursor } : {}),
|
|
3846
|
+
(page) => page.prompts
|
|
3847
|
+
);
|
|
3848
|
+
} catch (err) {
|
|
3849
|
+
slot.prompts = void 0;
|
|
3850
|
+
ctx.recordFailure(slot, "protocol", "prompt-discovery-failed");
|
|
3851
|
+
ctx.log.warn(`MCP server "${slot.cfg.name}" prompt discovery failed`, err);
|
|
3852
|
+
}
|
|
3853
|
+
} else {
|
|
3854
|
+
slot.prompts = void 0;
|
|
3855
|
+
}
|
|
3856
|
+
const durationMs = Date.now() - startedAt;
|
|
3857
|
+
pushBounded(slot.operations.discoverySamples, durationMs, MCP_OPERATION_LIMITS.LATENCY_SAMPLES);
|
|
3858
|
+
ctx.recordOperation(slot, "discover", "complete", void 0, durationMs, false);
|
|
3859
|
+
}
|
|
3860
|
+
async function persistSlotCapabilityManifest(cacheDir, slot) {
|
|
3861
|
+
if (!slot.lazy || !cacheDir) return;
|
|
3862
|
+
const previous = slot.manifestWrite ?? Promise.resolve();
|
|
3863
|
+
const pending = previous.then(
|
|
3864
|
+
() => writeCapabilityManifest(cacheDir, slot.cfg.name, manifestConfigHash(slot.cfg), {
|
|
3865
|
+
tools: slot.client?.listTools() ?? [],
|
|
3866
|
+
serverMetadata: slot.serverMetadata,
|
|
3867
|
+
resources: slot.resources,
|
|
3868
|
+
resourceTemplates: slot.resourceTemplates,
|
|
3869
|
+
prompts: slot.prompts
|
|
3870
|
+
})
|
|
3871
|
+
);
|
|
3872
|
+
slot.manifestWrite = pending;
|
|
3873
|
+
await pending;
|
|
3874
|
+
if (slot.manifestWrite === pending) slot.manifestWrite = void 0;
|
|
3875
|
+
}
|
|
3876
|
+
async function attemptConnectSlot(ctx, slot) {
|
|
3877
|
+
const MAX_ATTEMPTS = MCP_CONSTANTS.RECONNECT.MAX_ATTEMPTS;
|
|
3878
|
+
let attempt = 0;
|
|
3879
|
+
while (attempt < MAX_ATTEMPTS) {
|
|
3880
|
+
if (ctx.servers.has(slot.cfg.name) && ctx.servers.get(slot.cfg.name) !== slot) {
|
|
3881
|
+
return;
|
|
3882
|
+
}
|
|
3883
|
+
attempt++;
|
|
3884
|
+
const startedAt = Date.now();
|
|
3885
|
+
slot.state = attempt === 1 ? "connecting" : "reconnecting";
|
|
3886
|
+
slot.attempts = attempt;
|
|
3887
|
+
let client;
|
|
3888
|
+
let boundDisconnect;
|
|
3889
|
+
try {
|
|
3890
|
+
client = new MCPClient({
|
|
3891
|
+
name: slot.cfg.name,
|
|
3892
|
+
transport: slot.cfg.transport,
|
|
3893
|
+
command: slot.cfg.command,
|
|
3894
|
+
args: slot.cfg.args,
|
|
3895
|
+
env: slot.cfg.env,
|
|
3896
|
+
url: slot.cfg.url,
|
|
3897
|
+
headers: slot.cfg.headers,
|
|
3898
|
+
startupTimeoutMs: slot.cfg.startupTimeoutMs,
|
|
3899
|
+
requestTimeoutMs: slot.cfg.requestTimeoutMs,
|
|
3900
|
+
passthroughEnv: slot.cfg.passthroughEnv,
|
|
3901
|
+
authorizationProvider: ctx.authorizationProviderFactory?.(slot.cfg)
|
|
3902
|
+
});
|
|
3903
|
+
if (slot.cfg.transport === "stdio") {
|
|
3904
|
+
client.addExitListener(ctx.onChildExit);
|
|
3905
|
+
} else {
|
|
3906
|
+
boundDisconnect = () => ctx.onTransportDisconnect(slot.cfg.name);
|
|
3907
|
+
client.addDisconnectListener(boundDisconnect);
|
|
3908
|
+
}
|
|
3909
|
+
client.addToolsChangedListener(ctx.onToolsChanged);
|
|
3910
|
+
ctx.addCatalogListeners(client);
|
|
3911
|
+
await client.connect();
|
|
3912
|
+
if (slot.state === "disconnected" || ctx.servers.has(slot.cfg.name) && ctx.servers.get(slot.cfg.name) !== slot) {
|
|
3913
|
+
client.removeExitListener(ctx.onChildExit);
|
|
3914
|
+
if (boundDisconnect) client.removeDisconnectListener(boundDisconnect);
|
|
3915
|
+
client.removeToolsChangedListener(ctx.onToolsChanged);
|
|
3916
|
+
ctx.removeCatalogListeners(client);
|
|
3917
|
+
await client.close().catch(() => {
|
|
3918
|
+
});
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
if (slot.client && slot.client !== client) {
|
|
3922
|
+
const prior = slot.client;
|
|
3923
|
+
const priorDisconnect = slot.onDisconnect;
|
|
3924
|
+
slot.client.removeExitListener(ctx.onChildExit);
|
|
3925
|
+
if (priorDisconnect) prior.removeDisconnectListener(priorDisconnect);
|
|
3926
|
+
prior.removeToolsChangedListener(ctx.onToolsChanged);
|
|
3927
|
+
ctx.removeCatalogListeners(prior);
|
|
3928
|
+
prior.close().catch(() => {
|
|
3929
|
+
});
|
|
3930
|
+
}
|
|
3931
|
+
slot.client = client;
|
|
3932
|
+
slot.onDisconnect = boundDisconnect;
|
|
3933
|
+
const isReconnect = slot.reconnectCycles > 0 || attempt > 1;
|
|
3934
|
+
slot.state = "connected";
|
|
3935
|
+
slot.reconnectCycles = 0;
|
|
3936
|
+
const mc = client;
|
|
3937
|
+
const discovered = mc.listTools();
|
|
3938
|
+
await discoverSlotCapabilities(ctx, slot, mc);
|
|
3939
|
+
await persistSlotCapabilityManifest(ctx.cacheDir, slot);
|
|
3940
|
+
applySlotTools(ctx, slot, discovered, mc);
|
|
3941
|
+
const durationMs = Date.now() - startedAt;
|
|
3942
|
+
pushBounded(
|
|
3943
|
+
slot.operations.connectionSamples,
|
|
3944
|
+
durationMs,
|
|
3945
|
+
MCP_OPERATION_LIMITS.LATENCY_SAMPLES
|
|
3946
|
+
);
|
|
3947
|
+
ctx.recordSuccess(slot, (slot.operations.lastFailureAt ?? 0) < startedAt);
|
|
3948
|
+
ctx.recordOperation(
|
|
3949
|
+
slot,
|
|
3950
|
+
isReconnect ? "reconnect" : "connect",
|
|
3951
|
+
"connected",
|
|
3952
|
+
void 0,
|
|
3953
|
+
durationMs
|
|
3954
|
+
);
|
|
3955
|
+
slot.lastUsed = Date.now();
|
|
3956
|
+
if (slot.lazy) ctx.ensureIdleSweep();
|
|
3957
|
+
ctx.events.emit(isReconnect ? "mcp.server.reconnected" : "mcp.server.connected", {
|
|
3958
|
+
name: slot.cfg.name,
|
|
3959
|
+
toolCount: slot.toolNames.length
|
|
3960
|
+
});
|
|
3961
|
+
return;
|
|
3962
|
+
} catch (err) {
|
|
3963
|
+
ctx.recordFailure(slot, "transport", "connect-attempt-failed", Date.now() - startedAt);
|
|
3964
|
+
ctx.log.warn(`MCP server "${slot.cfg.name}" connect attempt ${attempt} failed`, err);
|
|
3965
|
+
if (client) {
|
|
3966
|
+
client.removeExitListener(ctx.onChildExit);
|
|
3967
|
+
if (boundDisconnect) client.removeDisconnectListener(boundDisconnect);
|
|
3968
|
+
client.removeToolsChangedListener(ctx.onToolsChanged);
|
|
3969
|
+
ctx.removeCatalogListeners(client);
|
|
3970
|
+
await client.close().catch(() => {
|
|
3971
|
+
});
|
|
3972
|
+
}
|
|
3973
|
+
if (attempt >= MAX_ATTEMPTS) {
|
|
3974
|
+
ctx.log.error(
|
|
3975
|
+
`MCP server "${slot.cfg.name}" connect exhausted after ${MAX_ATTEMPTS} attempts`,
|
|
3976
|
+
err
|
|
3977
|
+
);
|
|
3978
|
+
slot.state = "failed";
|
|
3979
|
+
slot.client = void 0;
|
|
3980
|
+
if (slot.reconnectTimer) {
|
|
3981
|
+
clearTimeout(slot.reconnectTimer);
|
|
3982
|
+
slot.reconnectTimer = void 0;
|
|
3983
|
+
}
|
|
3984
|
+
slot.reconnectPending = false;
|
|
3985
|
+
ctx.events.emit("mcp.server.disconnected", {
|
|
3986
|
+
name: slot.cfg.name,
|
|
3987
|
+
reason: err instanceof Error ? err.message : "unknown"
|
|
3988
|
+
});
|
|
3989
|
+
return;
|
|
3990
|
+
}
|
|
3991
|
+
const delay = 500 * 2 ** attempt;
|
|
3992
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
3993
|
+
if (slot.state === "disconnected" || ctx.servers.has(slot.cfg.name) && ctx.servers.get(slot.cfg.name) !== slot) {
|
|
3994
|
+
return;
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
// src/registry-idle.ts
|
|
4001
|
+
async function sleepIdleSlot(ctx, slot) {
|
|
4002
|
+
slot.reconnectPending = false;
|
|
4003
|
+
if (slot.reconnectTimer) {
|
|
4004
|
+
clearTimeout(slot.reconnectTimer);
|
|
4005
|
+
slot.reconnectTimer = void 0;
|
|
4006
|
+
}
|
|
4007
|
+
if (slot.client) {
|
|
4008
|
+
slot.client.removeExitListener(ctx.onChildExit);
|
|
4009
|
+
if (slot.onDisconnect) slot.client.removeDisconnectListener(slot.onDisconnect);
|
|
4010
|
+
slot.client.removeToolsChangedListener(ctx.onToolsChanged);
|
|
4011
|
+
ctx.removeCatalogListeners(slot.client);
|
|
4012
|
+
await slot.client.close();
|
|
4013
|
+
slot.client = void 0;
|
|
4014
|
+
}
|
|
4015
|
+
slot.onDisconnect = void 0;
|
|
4016
|
+
slot.state = "dormant";
|
|
4017
|
+
slot.operations.sleepCount++;
|
|
4018
|
+
ctx.recordOperation(slot, "sleep", "idle-timeout");
|
|
4019
|
+
ctx.log.info(`MCP server "${slot.cfg.name}" idle \u2014 sleeping (tools stay registered)`);
|
|
4020
|
+
ctx.events.emit("mcp.server.disconnected", { name: slot.cfg.name, reason: "idle-sleep" });
|
|
4021
|
+
}
|
|
4022
|
+
async function sweepIdleSlots(ctx) {
|
|
4023
|
+
if (ctx.idleTimeoutMs <= 0) return false;
|
|
4024
|
+
const now = Date.now();
|
|
4025
|
+
for (const slot of ctx.servers.values()) {
|
|
4026
|
+
if (slot.lazy && slot.state === "connected" && slot.client && now - slot.lastUsed > ctx.idleTimeoutMs) {
|
|
4027
|
+
await sleepIdleSlot(ctx, slot);
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
return [...ctx.servers.values()].some(
|
|
4031
|
+
(slot) => slot.lazy && slot.state === "connected" && slot.client
|
|
4032
|
+
);
|
|
4033
|
+
}
|
|
4034
|
+
|
|
3764
4035
|
// src/registry.ts
|
|
3765
4036
|
var MCPRegistry = class _MCPRegistry {
|
|
3766
4037
|
servers = /* @__PURE__ */ new Map();
|
|
@@ -3871,7 +4142,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
3871
4142
|
* cache yet, do a one-time cold discovery connect to learn + cache the tools.
|
|
3872
4143
|
*/
|
|
3873
4144
|
async startLazy(slot) {
|
|
3874
|
-
const cacheDir =
|
|
4145
|
+
const cacheDir = expectDefined2(this.cacheDir);
|
|
3875
4146
|
const hash = manifestConfigHash(slot.cfg);
|
|
3876
4147
|
const cached = await readCapabilityManifest(cacheDir, slot.cfg.name, hash);
|
|
3877
4148
|
if (cached) {
|
|
@@ -4118,121 +4389,45 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4118
4389
|
toolNamesForSlot(s) {
|
|
4119
4390
|
return s.toolNames.length > 0 ? s.toolNames.slice() : (s.lazyTools ?? []).map((t) => t.name);
|
|
4120
4391
|
}
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
(
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
},
|
|
4143
|
-
onFinish: ({ durationMs, ok }) => {
|
|
4144
|
-
slot.operations.inFlightCalls = Math.max(0, slot.operations.inFlightCalls - 1);
|
|
4145
|
-
pushBounded(
|
|
4146
|
-
slot.operations.callSamples,
|
|
4147
|
-
durationMs,
|
|
4148
|
-
MCP_OPERATION_LIMITS.LATENCY_SAMPLES
|
|
4149
|
-
);
|
|
4150
|
-
if (ok) {
|
|
4151
|
-
this.recordSuccess(slot);
|
|
4152
|
-
this.recordOperation(slot, "call", "ok", void 0, durationMs, false);
|
|
4153
|
-
} else {
|
|
4154
|
-
this.recordFailure(slot, "tool", "tool-call-failed", durationMs);
|
|
4155
|
-
}
|
|
4156
|
-
}
|
|
4157
|
-
})
|
|
4158
|
-
);
|
|
4159
|
-
if (this.lazyMode) {
|
|
4160
|
-
slot.lazyTools = wrapped;
|
|
4161
|
-
return;
|
|
4162
|
-
}
|
|
4163
|
-
for (const tool of wrapped) {
|
|
4164
|
-
try {
|
|
4165
|
-
this.toolRegistry.register(tool, `mcp:${slot.cfg.name}`);
|
|
4166
|
-
slot.toolNames.push(tool.name);
|
|
4167
|
-
} catch (err) {
|
|
4168
|
-
this.log.warn(`MCP tool "${tool.name}" not registered`, err);
|
|
4169
|
-
}
|
|
4170
|
-
}
|
|
4171
|
-
if (slot.lazy && wrapped.length > 0) slot.registeredLazy = true;
|
|
4392
|
+
connectContext() {
|
|
4393
|
+
return {
|
|
4394
|
+
servers: this.servers,
|
|
4395
|
+
toolRegistry: this.toolRegistry,
|
|
4396
|
+
events: this.events,
|
|
4397
|
+
log: this.log,
|
|
4398
|
+
lazyMode: this.lazyMode,
|
|
4399
|
+
cacheDir: this.cacheDir,
|
|
4400
|
+
authorizationProviderFactory: this.authorizationProviderFactory,
|
|
4401
|
+
operationListeners: this.operationListeners,
|
|
4402
|
+
ensureConnected: (name) => this.ensureConnected(name),
|
|
4403
|
+
recordOperation: (slot, kind, reason, failureKind, durationMs, retain) => this.recordOperation(slot, kind, reason, failureKind, durationMs, retain),
|
|
4404
|
+
recordSuccess: (slot, resetFailures) => this.recordSuccess(slot, resetFailures),
|
|
4405
|
+
recordFailure: (slot, failureKind, reason, durationMs) => this.recordFailure(slot, failureKind, reason, durationMs),
|
|
4406
|
+
onChildExit: this.onChildExit,
|
|
4407
|
+
onTransportDisconnect: this.onTransportDisconnect,
|
|
4408
|
+
onToolsChanged: this.onToolsChanged,
|
|
4409
|
+
addCatalogListeners: (client) => this.addCatalogListeners(client),
|
|
4410
|
+
removeCatalogListeners: (client) => this.removeCatalogListeners(client),
|
|
4411
|
+
ensureIdleSweep: () => this.ensureIdleSweep()
|
|
4412
|
+
};
|
|
4172
4413
|
}
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
}
|
|
4188
|
-
try {
|
|
4189
|
-
slot.resourceTemplates = await collectCatalogPages(
|
|
4190
|
-
(cursor) => client.listResourceTemplates(cursor ? { cursor } : {}),
|
|
4191
|
-
(page) => page.resourceTemplates
|
|
4192
|
-
);
|
|
4193
|
-
} catch (err) {
|
|
4194
|
-
slot.resourceTemplates = void 0;
|
|
4195
|
-
this.recordFailure(slot, "protocol", "resource-template-discovery-failed");
|
|
4196
|
-
this.log.warn(`MCP server "${slot.cfg.name}" resource template discovery failed`, err);
|
|
4197
|
-
}
|
|
4198
|
-
} else {
|
|
4199
|
-
slot.resources = void 0;
|
|
4200
|
-
slot.resourceTemplates = void 0;
|
|
4201
|
-
}
|
|
4202
|
-
if (capabilities?.prompts) {
|
|
4203
|
-
try {
|
|
4204
|
-
slot.prompts = await collectCatalogPages(
|
|
4205
|
-
(cursor) => client.listPrompts(cursor ? { cursor } : {}),
|
|
4206
|
-
(page) => page.prompts
|
|
4207
|
-
);
|
|
4208
|
-
} catch (err) {
|
|
4209
|
-
slot.prompts = void 0;
|
|
4210
|
-
this.recordFailure(slot, "protocol", "prompt-discovery-failed");
|
|
4211
|
-
this.log.warn(`MCP server "${slot.cfg.name}" prompt discovery failed`, err);
|
|
4212
|
-
}
|
|
4213
|
-
} else {
|
|
4214
|
-
slot.prompts = void 0;
|
|
4215
|
-
}
|
|
4216
|
-
const durationMs = Date.now() - startedAt;
|
|
4217
|
-
pushBounded(slot.operations.discoverySamples, durationMs, MCP_OPERATION_LIMITS.LATENCY_SAMPLES);
|
|
4218
|
-
this.recordOperation(slot, "discover", "complete", void 0, durationMs, false);
|
|
4414
|
+
idleContext() {
|
|
4415
|
+
return {
|
|
4416
|
+
servers: this.servers,
|
|
4417
|
+
idleTimeoutMs: this.idleTimeoutMs,
|
|
4418
|
+
events: this.events,
|
|
4419
|
+
log: this.log,
|
|
4420
|
+
recordOperation: (slot, kind, reason) => this.recordOperation(slot, kind, reason),
|
|
4421
|
+
onChildExit: this.onChildExit,
|
|
4422
|
+
onToolsChanged: this.onToolsChanged,
|
|
4423
|
+
removeCatalogListeners: (client) => this.removeCatalogListeners(client)
|
|
4424
|
+
};
|
|
4425
|
+
}
|
|
4426
|
+
applyTools(slot, tools, client) {
|
|
4427
|
+
applySlotTools(this.connectContext(), slot, tools, client);
|
|
4219
4428
|
}
|
|
4220
4429
|
async persistCapabilityManifest(slot) {
|
|
4221
|
-
|
|
4222
|
-
const cacheDir = this.cacheDir;
|
|
4223
|
-
const previous = slot.manifestWrite ?? Promise.resolve();
|
|
4224
|
-
const pending = previous.then(
|
|
4225
|
-
() => writeCapabilityManifest(cacheDir, slot.cfg.name, manifestConfigHash(slot.cfg), {
|
|
4226
|
-
tools: slot.client?.listTools() ?? [],
|
|
4227
|
-
serverMetadata: slot.serverMetadata,
|
|
4228
|
-
resources: slot.resources,
|
|
4229
|
-
resourceTemplates: slot.resourceTemplates,
|
|
4230
|
-
prompts: slot.prompts
|
|
4231
|
-
})
|
|
4232
|
-
);
|
|
4233
|
-
slot.manifestWrite = pending;
|
|
4234
|
-
await pending;
|
|
4235
|
-
if (slot.manifestWrite === pending) slot.manifestWrite = void 0;
|
|
4430
|
+
return persistSlotCapabilityManifest(this.cacheDir, slot);
|
|
4236
4431
|
}
|
|
4237
4432
|
/** Start the shared idle sweep timer once (unref'd so it never holds the process). */
|
|
4238
4433
|
ensureIdleSweep() {
|
|
@@ -4242,49 +4437,14 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4242
4437
|
}, MCP_CONSTANTS.IDLE.SWEEP_INTERVAL_MS);
|
|
4243
4438
|
this.idleTimer.unref?.();
|
|
4244
4439
|
}
|
|
4245
|
-
/** Auto-sleep connected lazy servers that have been idle past the timeout. */
|
|
4246
4440
|
async sweepIdle() {
|
|
4247
4441
|
if (this.idleTimeoutMs <= 0) return;
|
|
4248
|
-
const
|
|
4249
|
-
|
|
4250
|
-
if (slot.lazy && slot.state === "connected" && slot.client && now - slot.lastUsed > this.idleTimeoutMs) {
|
|
4251
|
-
await this.sleepIdle(slot);
|
|
4252
|
-
}
|
|
4253
|
-
}
|
|
4254
|
-
const hasConnectedLazyServer = [...this.servers.values()].some(
|
|
4255
|
-
(slot) => slot.lazy && slot.state === "connected" && slot.client
|
|
4256
|
-
);
|
|
4257
|
-
if (!hasConnectedLazyServer && this.idleTimer) {
|
|
4442
|
+
const hasConnectedLazy = await sweepIdleSlots(this.idleContext());
|
|
4443
|
+
if (!hasConnectedLazy && this.idleTimer) {
|
|
4258
4444
|
clearInterval(this.idleTimer);
|
|
4259
4445
|
this.idleTimer = void 0;
|
|
4260
4446
|
}
|
|
4261
4447
|
}
|
|
4262
|
-
/**
|
|
4263
|
-
* Soft stop: close the server process but KEEP its resolver wrappers and
|
|
4264
|
-
* cached manifest registered, so the next tool call transparently re-wakes it.
|
|
4265
|
-
* Distinct from {@link stop} (full teardown for disable/remove).
|
|
4266
|
-
*/
|
|
4267
|
-
async sleepIdle(slot) {
|
|
4268
|
-
slot.reconnectPending = false;
|
|
4269
|
-
if (slot.reconnectTimer) {
|
|
4270
|
-
clearTimeout(slot.reconnectTimer);
|
|
4271
|
-
slot.reconnectTimer = void 0;
|
|
4272
|
-
}
|
|
4273
|
-
if (slot.client) {
|
|
4274
|
-
slot.client.removeExitListener(this.onChildExit);
|
|
4275
|
-
if (slot.onDisconnect) slot.client.removeDisconnectListener(slot.onDisconnect);
|
|
4276
|
-
slot.client.removeToolsChangedListener(this.onToolsChanged);
|
|
4277
|
-
this.removeCatalogListeners(slot.client);
|
|
4278
|
-
await slot.client.close();
|
|
4279
|
-
slot.client = void 0;
|
|
4280
|
-
}
|
|
4281
|
-
slot.onDisconnect = void 0;
|
|
4282
|
-
slot.state = "dormant";
|
|
4283
|
-
slot.operations.sleepCount++;
|
|
4284
|
-
this.recordOperation(slot, "sleep", "idle-timeout");
|
|
4285
|
-
this.log.info(`MCP server "${slot.cfg.name}" idle \u2014 sleeping (tools stay registered)`);
|
|
4286
|
-
this.events.emit("mcp.server.disconnected", { name: slot.cfg.name, reason: "idle-sleep" });
|
|
4287
|
-
}
|
|
4288
4448
|
/**
|
|
4289
4449
|
* Catalog of every server ever registered with this registry — includes
|
|
4290
4450
|
* servers that are stopped, failed, or not yet started.
|
|
@@ -4452,134 +4612,14 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4452
4612
|
);
|
|
4453
4613
|
}
|
|
4454
4614
|
async attemptConnect(slot) {
|
|
4455
|
-
|
|
4456
|
-
let attempt = 0;
|
|
4457
|
-
while (attempt < MAX_ATTEMPTS) {
|
|
4458
|
-
if (this.servers.has(slot.cfg.name) && this.servers.get(slot.cfg.name) !== slot) {
|
|
4459
|
-
return;
|
|
4460
|
-
}
|
|
4461
|
-
attempt++;
|
|
4462
|
-
const startedAt = Date.now();
|
|
4463
|
-
slot.state = attempt === 1 ? "connecting" : "reconnecting";
|
|
4464
|
-
slot.attempts = attempt;
|
|
4465
|
-
let client;
|
|
4466
|
-
let boundDisconnect;
|
|
4467
|
-
try {
|
|
4468
|
-
client = new MCPClient({
|
|
4469
|
-
name: slot.cfg.name,
|
|
4470
|
-
transport: slot.cfg.transport,
|
|
4471
|
-
command: slot.cfg.command,
|
|
4472
|
-
args: slot.cfg.args,
|
|
4473
|
-
env: slot.cfg.env,
|
|
4474
|
-
url: slot.cfg.url,
|
|
4475
|
-
headers: slot.cfg.headers,
|
|
4476
|
-
startupTimeoutMs: slot.cfg.startupTimeoutMs,
|
|
4477
|
-
requestTimeoutMs: slot.cfg.requestTimeoutMs,
|
|
4478
|
-
passthroughEnv: slot.cfg.passthroughEnv,
|
|
4479
|
-
authorizationProvider: this.authorizationProviderFactory?.(slot.cfg)
|
|
4480
|
-
});
|
|
4481
|
-
if (slot.cfg.transport === "stdio") {
|
|
4482
|
-
client.addExitListener(this.onChildExit);
|
|
4483
|
-
} else {
|
|
4484
|
-
boundDisconnect = () => this.onTransportDisconnect(slot.cfg.name);
|
|
4485
|
-
client.addDisconnectListener(boundDisconnect);
|
|
4486
|
-
}
|
|
4487
|
-
client.addToolsChangedListener(this.onToolsChanged);
|
|
4488
|
-
this.addCatalogListeners(client);
|
|
4489
|
-
await client.connect();
|
|
4490
|
-
if (slot.state === "disconnected" || this.servers.has(slot.cfg.name) && this.servers.get(slot.cfg.name) !== slot) {
|
|
4491
|
-
client.removeExitListener(this.onChildExit);
|
|
4492
|
-
if (boundDisconnect) client.removeDisconnectListener(boundDisconnect);
|
|
4493
|
-
client.removeToolsChangedListener(this.onToolsChanged);
|
|
4494
|
-
this.removeCatalogListeners(client);
|
|
4495
|
-
await client.close().catch(() => {
|
|
4496
|
-
});
|
|
4497
|
-
return;
|
|
4498
|
-
}
|
|
4499
|
-
if (slot.client && slot.client !== client) {
|
|
4500
|
-
const prior = slot.client;
|
|
4501
|
-
const priorDisconnect = slot.onDisconnect;
|
|
4502
|
-
slot.client.removeExitListener(this.onChildExit);
|
|
4503
|
-
if (priorDisconnect) prior.removeDisconnectListener(priorDisconnect);
|
|
4504
|
-
prior.removeToolsChangedListener(this.onToolsChanged);
|
|
4505
|
-
this.removeCatalogListeners(prior);
|
|
4506
|
-
prior.close().catch(() => {
|
|
4507
|
-
});
|
|
4508
|
-
}
|
|
4509
|
-
slot.client = client;
|
|
4510
|
-
slot.onDisconnect = boundDisconnect;
|
|
4511
|
-
const isReconnect = slot.reconnectCycles > 0 || attempt > 1;
|
|
4512
|
-
slot.state = "connected";
|
|
4513
|
-
slot.reconnectCycles = 0;
|
|
4514
|
-
const mc = client;
|
|
4515
|
-
const discovered = mc.listTools();
|
|
4516
|
-
await this.discoverCapabilities(slot, mc);
|
|
4517
|
-
await this.persistCapabilityManifest(slot);
|
|
4518
|
-
this.applyTools(slot, discovered, mc);
|
|
4519
|
-
const durationMs = Date.now() - startedAt;
|
|
4520
|
-
pushBounded(
|
|
4521
|
-
slot.operations.connectionSamples,
|
|
4522
|
-
durationMs,
|
|
4523
|
-
MCP_OPERATION_LIMITS.LATENCY_SAMPLES
|
|
4524
|
-
);
|
|
4525
|
-
this.recordSuccess(slot, (slot.operations.lastFailureAt ?? 0) < startedAt);
|
|
4526
|
-
this.recordOperation(
|
|
4527
|
-
slot,
|
|
4528
|
-
isReconnect ? "reconnect" : "connect",
|
|
4529
|
-
"connected",
|
|
4530
|
-
void 0,
|
|
4531
|
-
durationMs
|
|
4532
|
-
);
|
|
4533
|
-
slot.lastUsed = Date.now();
|
|
4534
|
-
if (slot.lazy) this.ensureIdleSweep();
|
|
4535
|
-
this.events.emit(isReconnect ? "mcp.server.reconnected" : "mcp.server.connected", {
|
|
4536
|
-
name: slot.cfg.name,
|
|
4537
|
-
toolCount: slot.toolNames.length
|
|
4538
|
-
});
|
|
4539
|
-
return;
|
|
4540
|
-
} catch (err) {
|
|
4541
|
-
this.recordFailure(slot, "transport", "connect-attempt-failed", Date.now() - startedAt);
|
|
4542
|
-
this.log.warn(`MCP server "${slot.cfg.name}" connect attempt ${attempt} failed`, err);
|
|
4543
|
-
if (client) {
|
|
4544
|
-
client.removeExitListener(this.onChildExit);
|
|
4545
|
-
if (boundDisconnect) client.removeDisconnectListener(boundDisconnect);
|
|
4546
|
-
client.removeToolsChangedListener(this.onToolsChanged);
|
|
4547
|
-
this.removeCatalogListeners(client);
|
|
4548
|
-
await client.close().catch(() => {
|
|
4549
|
-
});
|
|
4550
|
-
}
|
|
4551
|
-
if (attempt >= MAX_ATTEMPTS) {
|
|
4552
|
-
this.log.error(
|
|
4553
|
-
`MCP server "${slot.cfg.name}" connect exhausted after ${MAX_ATTEMPTS} attempts`,
|
|
4554
|
-
err
|
|
4555
|
-
);
|
|
4556
|
-
slot.state = "failed";
|
|
4557
|
-
slot.client = void 0;
|
|
4558
|
-
if (slot.reconnectTimer) {
|
|
4559
|
-
clearTimeout(slot.reconnectTimer);
|
|
4560
|
-
slot.reconnectTimer = void 0;
|
|
4561
|
-
}
|
|
4562
|
-
slot.reconnectPending = false;
|
|
4563
|
-
this.events.emit("mcp.server.disconnected", {
|
|
4564
|
-
name: slot.cfg.name,
|
|
4565
|
-
reason: err instanceof Error ? err.message : "unknown"
|
|
4566
|
-
});
|
|
4567
|
-
return;
|
|
4568
|
-
}
|
|
4569
|
-
const delay = 500 * 2 ** attempt;
|
|
4570
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
4571
|
-
if (slot.state === "disconnected" || this.servers.has(slot.cfg.name) && this.servers.get(slot.cfg.name) !== slot) {
|
|
4572
|
-
return;
|
|
4573
|
-
}
|
|
4574
|
-
}
|
|
4575
|
-
}
|
|
4615
|
+
return attemptConnectSlot(this.connectContext(), slot);
|
|
4576
4616
|
}
|
|
4577
4617
|
};
|
|
4578
4618
|
|
|
4579
4619
|
// src/server.ts
|
|
4580
4620
|
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
4581
4621
|
import { createServer } from "node:http";
|
|
4582
|
-
import { expectDefined as
|
|
4622
|
+
import { expectDefined as expectDefined3, toErrorMessage as toErrorMessage2, validateAgainstSchema } from "@wrongstack/core/utils";
|
|
4583
4623
|
var PARSE_ERROR = -32700;
|
|
4584
4624
|
var INVALID_REQUEST = -32600;
|
|
4585
4625
|
var METHOD_NOT_FOUND = -32601;
|
|
@@ -4627,7 +4667,7 @@ var MCPServer = class {
|
|
|
4627
4667
|
const result = await this.dispatch(msg.method, msg.params);
|
|
4628
4668
|
if (result === METHOD_NOT_FOUND_SENTINEL) {
|
|
4629
4669
|
return this.encodeError(
|
|
4630
|
-
|
|
4670
|
+
expectDefined3(msg.id),
|
|
4631
4671
|
METHOD_NOT_FOUND,
|
|
4632
4672
|
`Method not found: ${msg.method}`
|
|
4633
4673
|
);
|
|
@@ -4637,7 +4677,7 @@ var MCPServer = class {
|
|
|
4637
4677
|
const message = toErrorMessage2(err);
|
|
4638
4678
|
this.logger?.warn?.(`MCP server: method "${msg.method}" threw: ${message}`);
|
|
4639
4679
|
const code = err instanceof InvalidToolArgumentsError ? INVALID_PARAMS : INTERNAL_ERROR;
|
|
4640
|
-
return this.encodeError(
|
|
4680
|
+
return this.encodeError(expectDefined3(msg.id), code, message);
|
|
4641
4681
|
}
|
|
4642
4682
|
}
|
|
4643
4683
|
async dispatch(method, params) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { EventBus } from '@wrongstack/core/kernel';
|
|
2
|
+
import type { ToolRegistry } from '@wrongstack/core/registry';
|
|
3
|
+
import type { Logger } from '@wrongstack/core/types';
|
|
4
|
+
import { MCPClient, type MCPTool } from './client.js';
|
|
5
|
+
import { type MCPFailureKind, type MCPOperationKind, type MCPOperationListener } from './operations.js';
|
|
6
|
+
import type { ServerSlot } from './registry-slots.js';
|
|
7
|
+
import type { MCPRegistryOptions } from './registry-types.js';
|
|
8
|
+
export interface RegistryConnectContext {
|
|
9
|
+
servers: Map<string, ServerSlot>;
|
|
10
|
+
toolRegistry: ToolRegistry;
|
|
11
|
+
events: EventBus;
|
|
12
|
+
log: Logger;
|
|
13
|
+
lazyMode: boolean;
|
|
14
|
+
cacheDir?: string | undefined;
|
|
15
|
+
authorizationProviderFactory?: MCPRegistryOptions['authorizationProviderFactory'] | undefined;
|
|
16
|
+
operationListeners: Set<MCPOperationListener>;
|
|
17
|
+
ensureConnected: (name: string) => Promise<MCPClient>;
|
|
18
|
+
recordOperation: (slot: ServerSlot, kind: MCPOperationKind, reason?: string, failureKind?: MCPFailureKind, durationMs?: number, retain?: boolean) => void;
|
|
19
|
+
recordSuccess: (slot: ServerSlot, resetFailures?: boolean) => void;
|
|
20
|
+
recordFailure: (slot: ServerSlot, failureKind: MCPFailureKind, reason: string, durationMs?: number) => void;
|
|
21
|
+
onChildExit: (name: string, code: number | null, signal: string | null) => void;
|
|
22
|
+
onTransportDisconnect: (name: string) => void;
|
|
23
|
+
onToolsChanged: (name: string, tools: {
|
|
24
|
+
name: string;
|
|
25
|
+
}[]) => void;
|
|
26
|
+
addCatalogListeners: (client: MCPClient) => void;
|
|
27
|
+
removeCatalogListeners: (client: MCPClient) => void;
|
|
28
|
+
ensureIdleSweep: () => void;
|
|
29
|
+
}
|
|
30
|
+
export declare function applySlotTools(ctx: RegistryConnectContext, slot: ServerSlot, tools: MCPTool[], client?: MCPClient | undefined): void;
|
|
31
|
+
export declare function discoverSlotCapabilities(ctx: RegistryConnectContext, slot: ServerSlot, client: MCPClient): Promise<void>;
|
|
32
|
+
export declare function persistSlotCapabilityManifest(cacheDir: string | undefined, slot: ServerSlot): Promise<void>;
|
|
33
|
+
export declare function attemptConnectSlot(ctx: RegistryConnectContext, slot: ServerSlot): Promise<void>;
|
|
34
|
+
//# sourceMappingURL=registry-connect-loop.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { EventBus } from '@wrongstack/core/kernel';
|
|
2
|
+
import type { Logger } from '@wrongstack/core/types';
|
|
3
|
+
import type { MCPClient } from './client.js';
|
|
4
|
+
import type { MCPOperationKind } from './operations.js';
|
|
5
|
+
import type { ServerSlot } from './registry-slots.js';
|
|
6
|
+
export interface RegistryIdleContext {
|
|
7
|
+
servers: Map<string, ServerSlot>;
|
|
8
|
+
idleTimeoutMs: number;
|
|
9
|
+
events: EventBus;
|
|
10
|
+
log: Logger;
|
|
11
|
+
recordOperation: (slot: ServerSlot, kind: MCPOperationKind, reason?: string) => void;
|
|
12
|
+
onChildExit: (name: string, code: number | null, signal: string | null) => void;
|
|
13
|
+
onToolsChanged: (name: string, tools: {
|
|
14
|
+
name: string;
|
|
15
|
+
}[]) => void;
|
|
16
|
+
removeCatalogListeners: (client: MCPClient) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare function sleepIdleSlot(ctx: RegistryIdleContext, slot: ServerSlot): Promise<void>;
|
|
19
|
+
export declare function sweepIdleSlots(ctx: RegistryIdleContext): Promise<boolean>;
|
|
20
|
+
//# sourceMappingURL=registry-idle.d.ts.map
|
package/dist/registry.d.ts
CHANGED
|
@@ -105,26 +105,13 @@ export declare class MCPRegistry {
|
|
|
105
105
|
* tools are connected but intentionally not registered).
|
|
106
106
|
*/
|
|
107
107
|
private toolNamesForSlot;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
* wrappers that spawn the process on first use; eager servers bind the live
|
|
111
|
-
* client directly. Honours token-saving `lazyMode` (cache, don't register) and
|
|
112
|
-
* a register-once guard for lazy resolver wrappers (so a wake/reconnect reuses
|
|
113
|
-
* the existing registrations rather than churning the tool list).
|
|
114
|
-
*/
|
|
108
|
+
private connectContext;
|
|
109
|
+
private idleContext;
|
|
115
110
|
private applyTools;
|
|
116
|
-
private discoverCapabilities;
|
|
117
111
|
private persistCapabilityManifest;
|
|
118
112
|
/** Start the shared idle sweep timer once (unref'd so it never holds the process). */
|
|
119
113
|
private ensureIdleSweep;
|
|
120
|
-
/** Auto-sleep connected lazy servers that have been idle past the timeout. */
|
|
121
114
|
private sweepIdle;
|
|
122
|
-
/**
|
|
123
|
-
* Soft stop: close the server process but KEEP its resolver wrappers and
|
|
124
|
-
* cached manifest registered, so the next tool call transparently re-wakes it.
|
|
125
|
-
* Distinct from {@link stop} (full teardown for disable/remove).
|
|
126
|
-
*/
|
|
127
|
-
private sleepIdle;
|
|
128
115
|
/**
|
|
129
116
|
* Catalog of every server ever registered with this registry — includes
|
|
130
117
|
* servers that are stopped, failed, or not yet started.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.307.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "WrongStack Model Context Protocol client and registry: stdio, SSE, and streamable HTTP transports.",
|
|
6
6
|
"repository": {
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"!dist/**/*.map"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@wrongstack/core": "0.
|
|
29
|
+
"@wrongstack/core": "0.307.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@types/node": "^26.
|
|
32
|
+
"@types/node": "^26.2.0",
|
|
33
33
|
"typescript": "^7.0.2",
|
|
34
|
-
"undici-types": "^8.
|
|
34
|
+
"undici-types": "^8.10.0",
|
|
35
35
|
"vitest": "^4.1.10"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|