@wrongstack/mcp 1.0.35 → 1.0.36
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-http-connection.d.ts +1 -0
- package/dist/client-stdio-request.d.ts +2 -0
- package/dist/client.d.ts +16 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +186 -77
- package/dist/protocol.d.ts +24 -0
- package/dist/sse-reader.d.ts +2 -1
- package/package.json +3 -3
|
@@ -10,6 +10,7 @@ export interface ClientHttpConnectionHost {
|
|
|
10
10
|
disconnectListeners: Set<() => void>;
|
|
11
11
|
_tools: MCPTool[];
|
|
12
12
|
_toolsCache: MCPTool[] | undefined;
|
|
13
|
+
readonly toolCatalogVersion: number;
|
|
13
14
|
toolsChangedListeners: Set<ToolsChangedListener>;
|
|
14
15
|
emitCapabilityChanged: (capability: 'resources' | 'prompts') => void;
|
|
15
16
|
emitResourceUpdated: (uri: string) => void;
|
|
@@ -3,6 +3,8 @@ import type { JsonRpcResponse } from './contracts.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* The effective per-request timeout: an explicit positive `timeoutMs` wins,
|
|
5
5
|
* then the client's configured `requestTimeoutMs`, then the package default.
|
|
6
|
+
* Capped at the largest timer delay: Node turns a longer one into 1 ms, which
|
|
7
|
+
* timed out every request to a server configured to "never" time out.
|
|
6
8
|
*/
|
|
7
9
|
export declare function resolveRequestTimeoutMs(configuredMs: number | undefined, timeoutMs: number | undefined): number;
|
|
8
10
|
/**
|
package/dist/client.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { StringDecoder } from 'node:string_decoder';
|
|
|
2
2
|
import type { ExitListener, JsonRpcServerRequest, MCPClientOptions, MCPListChangedListener, MCPLogMessageListener, MCPPageOptions, MCPProgressListener, MCPRequestOptions, MCPResourceUpdatedListener, ToolsChangedListener } from './client-types.js';
|
|
3
3
|
import type { ConnectionState, JsonRpcResponse, MCPTool, ToolCallResult } from './contracts.js';
|
|
4
4
|
import { ServerRequestResponder, type UrlElicitation } from './elicitation.js';
|
|
5
|
-
import type
|
|
5
|
+
import { type MCPCompletionReference, type MCPCompletionResult, type MCPGetPromptResult, type MCPListPromptsResult, type MCPListResourcesResult, type MCPListResourceTemplatesResult, type MCPReadResourceResult, type MCPServerMetadata } from './protocol.js';
|
|
6
6
|
export { forceKillTree } from './client-process.js';
|
|
7
7
|
export { quoteWindowsArg } from './client-protocol-helpers.js';
|
|
8
8
|
export type { ConnectionState, JsonRpcResponse, MCPTool, ToolCallResult };
|
|
@@ -109,6 +109,21 @@ export declare class MCPClient {
|
|
|
109
109
|
unsubscribeResource(uri: string, opts?: MCPRequestOptions): Promise<void>;
|
|
110
110
|
listPrompts(opts?: MCPPageOptions): Promise<MCPListPromptsResult>;
|
|
111
111
|
getPrompt(name: string, args?: Record<string, string> | undefined, opts?: MCPRequestOptions): Promise<MCPGetPromptResult>;
|
|
112
|
+
/**
|
|
113
|
+
* Liveness check (MCP `ping`): the server MUST answer promptly with an
|
|
114
|
+
* empty result. Works on every transport — a cheap connection health probe
|
|
115
|
+
* before a long call, and the natural reconnect-readiness check.
|
|
116
|
+
*/
|
|
117
|
+
ping(opts?: MCPRequestOptions): Promise<void>;
|
|
118
|
+
/**
|
|
119
|
+
* Argument autocompletion (MCP `completion/complete`, 2024-11-05). The
|
|
120
|
+
* server must advertise the `completions` capability; an unsupported
|
|
121
|
+
* server answers with a JSON-RPC error, which surfaces here as a throw.
|
|
122
|
+
*/
|
|
123
|
+
complete(ref: MCPCompletionReference, argument: {
|
|
124
|
+
name: string;
|
|
125
|
+
value: string;
|
|
126
|
+
}, opts?: MCPRequestOptions): Promise<MCPCompletionResult>;
|
|
112
127
|
close(): Promise<void>;
|
|
113
128
|
private closeInner;
|
|
114
129
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { elicitViaUserInput } from './elicitation-form.js';
|
|
|
12
12
|
export { addMcp, disableMcp, discoverMcp, enableMcp, listMcp, MCP_ENV_MASK, type McpManageDeps, type McpOpResult, type McpServerInfo, type McpServerInput, removeMcp, restartMcp, updateMcp, } from './manage.js';
|
|
13
13
|
export { type MCPCapabilityManifest, manifestConfigHash, readCapabilityManifest, readManifest, writeCapabilityManifest, writeManifest, } from './manifest-cache.js';
|
|
14
14
|
export { MCP_OPERATION_LIMITS, type MCPFailureKind, type MCPHealthState, type MCPLatencySummary, type MCPOperationEvent, type MCPOperationKind, type MCPServerOperationalHealth, } from './operations.js';
|
|
15
|
-
export { type MCPGetPromptResult, type MCPImplementationInfo, type MCPListPromptsResult, type MCPListResourcesResult, type MCPListResourceTemplatesResult, type MCPPrompt, type MCPPromptArgument, type MCPPromptMessage, type MCPReadResourceResult, type MCPResource, type MCPResourceContents, type MCPResourceTemplate, type MCPServerCapabilities, type MCPServerMetadata, parseGetPromptResult, parseListPromptsResult, parseListResourcesResult, parseListResourceTemplatesResult, parseReadResourceResult, parseServerMetadata, } from './protocol.js';
|
|
15
|
+
export { type MCPCompletionReference, type MCPCompletionResult, type MCPGetPromptResult, type MCPImplementationInfo, type MCPListPromptsResult, type MCPListResourcesResult, type MCPListResourceTemplatesResult, type MCPPrompt, type MCPPromptArgument, type MCPPromptMessage, type MCPReadResourceResult, type MCPResource, type MCPResourceContents, type MCPResourceTemplate, type MCPServerCapabilities, type MCPServerMetadata, parseCompletionResult, parseGetPromptResult, parseListPromptsResult, parseListResourcesResult, parseListResourceTemplatesResult, parseReadResourceResult, parseServerMetadata, } from './protocol.js';
|
|
16
16
|
export { MCPRegistry, type MCPRegistryCatalog, type MCPRegistryOptions } from './registry.js';
|
|
17
17
|
export { MCPServer, type MCPServerCallResult, type MCPServerLogger, type MCPServerOptions, type MCPServerPrompt, type MCPServerResource, type MCPServerTool, type MCPServerToolHost, type ServeHttpHandle, type ServeHttpOptions, type ServeStdioHandle, type ServeStdioOptions, serveHttp, serveStdio, toContentBlocks, } from './server.js';
|
|
18
18
|
export { createVaultBackedMcpAuthorizationProviderFactory, type MCPAuthorizationStateEvent, MCPRefreshingAuthorizationProvider, type MCPRefreshingAuthorizationProviderOptions, type MCPStoredAuthorization, type MCPVaultProviderFactoryOptions, MCPVaultTokenStore, } from './token-store.js';
|
package/dist/index.js
CHANGED
|
@@ -1443,6 +1443,24 @@ function logMessageNotification(params) {
|
|
|
1443
1443
|
data: input["data"] === void 0 ? void 0 : serializedNotificationData(input["data"])
|
|
1444
1444
|
};
|
|
1445
1445
|
}
|
|
1446
|
+
function parseCompletionResult(value) {
|
|
1447
|
+
const envelope = record2(value, "completion/complete result");
|
|
1448
|
+
const completion = record2(envelope["completion"], "completion/complete result.completion");
|
|
1449
|
+
const rawValues = completion["values"];
|
|
1450
|
+
if (!Array.isArray(rawValues)) {
|
|
1451
|
+
throw new Error("Malformed MCP completion/complete result: expected values array");
|
|
1452
|
+
}
|
|
1453
|
+
const values = rawValues.map((entry, index) => {
|
|
1454
|
+
if (typeof entry !== "string") {
|
|
1455
|
+
throw new Error(`Malformed MCP completion/complete result.values[${index}]: expected string`);
|
|
1456
|
+
}
|
|
1457
|
+
return entry;
|
|
1458
|
+
});
|
|
1459
|
+
const rawTotal = completion["total"];
|
|
1460
|
+
const total = typeof rawTotal === "number" && Number.isInteger(rawTotal) && rawTotal >= 0 ? rawTotal : void 0;
|
|
1461
|
+
const hasMore = typeof completion["hasMore"] === "boolean" ? completion["hasMore"] : void 0;
|
|
1462
|
+
return { completion: { values, total, hasMore } };
|
|
1463
|
+
}
|
|
1446
1464
|
|
|
1447
1465
|
// src/client-capabilities.ts
|
|
1448
1466
|
var MCPCapabilityClient = class {
|
|
@@ -1710,7 +1728,7 @@ var MCPServer = class {
|
|
|
1710
1728
|
}
|
|
1711
1729
|
const validId = msg?.id === void 0 || msg.id === null || typeof msg.id === "string" || typeof msg.id === "number";
|
|
1712
1730
|
if (typeof msg !== "object" || msg === null || msg.jsonrpc !== "2.0" || typeof msg.method !== "string" || !validId) {
|
|
1713
|
-
const id = msg && typeof msg === "object" ? msg.id ?? null : null;
|
|
1731
|
+
const id = validId && msg && typeof msg === "object" ? msg.id ?? null : null;
|
|
1714
1732
|
return this.encodeError(id ?? null, INVALID_REQUEST, "Invalid Request");
|
|
1715
1733
|
}
|
|
1716
1734
|
const isNotification = msg.id === void 0 || msg.id === null;
|
|
@@ -2166,10 +2184,11 @@ function checkValue(field, raw) {
|
|
|
2166
2184
|
switch (field.kind) {
|
|
2167
2185
|
case "string": {
|
|
2168
2186
|
if (typeof raw !== "string") return { ok: false, error: `${name} must be text` };
|
|
2169
|
-
|
|
2187
|
+
const length = [...raw].length;
|
|
2188
|
+
if (field.minLength !== void 0 && length < field.minLength) {
|
|
2170
2189
|
return { ok: false, error: `${name} needs at least ${field.minLength} characters` };
|
|
2171
2190
|
}
|
|
2172
|
-
if (field.maxLength !== void 0 &&
|
|
2191
|
+
if (field.maxLength !== void 0 && length > field.maxLength) {
|
|
2173
2192
|
return { ok: false, error: `${name} allows at most ${field.maxLength} characters` };
|
|
2174
2193
|
}
|
|
2175
2194
|
return { ok: true, value: raw };
|
|
@@ -2398,6 +2417,10 @@ async function listAllTools(requestPage) {
|
|
|
2398
2417
|
break;
|
|
2399
2418
|
}
|
|
2400
2419
|
const result = response.result;
|
|
2420
|
+
if (!Array.isArray(result?.tools)) {
|
|
2421
|
+
if (page === 0) return null;
|
|
2422
|
+
break;
|
|
2423
|
+
}
|
|
2401
2424
|
tools.push(...normalizeMCPTools(result?.tools));
|
|
2402
2425
|
const next = result?.nextCursor;
|
|
2403
2426
|
if (typeof next !== "string" || next.length === 0) break;
|
|
@@ -2538,6 +2561,7 @@ async function refreshToolsOnListChanged(self) {
|
|
|
2538
2561
|
self._toolsCache = tools;
|
|
2539
2562
|
self.toolCatalogRevision++;
|
|
2540
2563
|
for (const listener of self.toolsChangedListeners) {
|
|
2564
|
+
if (version !== self.toolCatalogVersion) break;
|
|
2541
2565
|
try {
|
|
2542
2566
|
listener(self.opts.name, [...tools]);
|
|
2543
2567
|
} catch {
|
|
@@ -2590,6 +2614,9 @@ function buildClientHttpConnectionHost(self) {
|
|
|
2590
2614
|
self._toolsCache = value;
|
|
2591
2615
|
},
|
|
2592
2616
|
toolsChangedListeners: self.toolsChangedListeners,
|
|
2617
|
+
get toolCatalogVersion() {
|
|
2618
|
+
return self.toolCatalogVersion;
|
|
2619
|
+
},
|
|
2593
2620
|
emitCapabilityChanged: (...args) => emitCapabilityChanged(self, ...args),
|
|
2594
2621
|
emitResourceUpdated: (uri) => emitResourceUpdated(self, uri),
|
|
2595
2622
|
emitProgress: (progress) => emitProgress(self, progress),
|
|
@@ -2714,11 +2741,59 @@ function buildClientStdioProtocolHost(self) {
|
|
|
2714
2741
|
}
|
|
2715
2742
|
|
|
2716
2743
|
// src/sse-reader.ts
|
|
2744
|
+
import { ToolError as ToolError2 } from "@wrongstack/core/types";
|
|
2745
|
+
|
|
2746
|
+
// src/read-body.ts
|
|
2717
2747
|
import { ToolError } from "@wrongstack/core/types";
|
|
2718
|
-
var
|
|
2748
|
+
var MAX_MCP_HTTP_BODY_BYTES = 16 * 1024 * 1024;
|
|
2749
|
+
async function readBodyCapped(res, maxBytes = MAX_MCP_HTTP_BODY_BYTES) {
|
|
2750
|
+
const safeCap = typeof maxBytes === "number" && Number.isFinite(maxBytes) && maxBytes >= 0 ? maxBytes : MAX_MCP_HTTP_BODY_BYTES;
|
|
2751
|
+
const body = res.body;
|
|
2752
|
+
if (!body || typeof body.getReader !== "function") {
|
|
2753
|
+
const text = await res.text();
|
|
2754
|
+
const received = Buffer.byteLength(text, "utf8");
|
|
2755
|
+
if (received > safeCap) {
|
|
2756
|
+
throw new ToolError({
|
|
2757
|
+
message: `MCP response body exceeded ${safeCap} bytes \u2014 refusing to buffer`,
|
|
2758
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
2759
|
+
toolName: "mcp_transport",
|
|
2760
|
+
context: { maxBytes: safeCap, received }
|
|
2761
|
+
});
|
|
2762
|
+
}
|
|
2763
|
+
return text;
|
|
2764
|
+
}
|
|
2765
|
+
const reader = body.getReader();
|
|
2766
|
+
const chunks = [];
|
|
2767
|
+
let total = 0;
|
|
2768
|
+
try {
|
|
2769
|
+
for (; ; ) {
|
|
2770
|
+
const { done, value } = await reader.read();
|
|
2771
|
+
if (done) break;
|
|
2772
|
+
if (!value) continue;
|
|
2773
|
+
total += value.byteLength;
|
|
2774
|
+
if (total > safeCap) {
|
|
2775
|
+
await reader.cancel().catch(() => void 0);
|
|
2776
|
+
throw new ToolError({
|
|
2777
|
+
message: `MCP response body exceeded ${safeCap} bytes \u2014 refusing to buffer`,
|
|
2778
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
2779
|
+
toolName: "mcp_transport",
|
|
2780
|
+
context: { maxBytes: safeCap, received: total }
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
chunks.push(value);
|
|
2784
|
+
}
|
|
2785
|
+
} finally {
|
|
2786
|
+
reader.releaseLock?.();
|
|
2787
|
+
}
|
|
2788
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
// src/sse-reader.ts
|
|
2792
|
+
var SSE_READER_MAX_BUFFER = MAX_MCP_HTTP_BODY_BYTES;
|
|
2719
2793
|
var SSE_READER_MAX_DATA_LINES = 1024;
|
|
2720
2794
|
var SSEReader = class {
|
|
2721
|
-
|
|
2795
|
+
pending = [];
|
|
2796
|
+
pendingLength = 0;
|
|
2722
2797
|
skipLeadingLF = false;
|
|
2723
2798
|
dataLines = [];
|
|
2724
2799
|
dataLength = 0;
|
|
@@ -2745,47 +2820,58 @@ var SSEReader = class {
|
|
|
2745
2820
|
};
|
|
2746
2821
|
}
|
|
2747
2822
|
feed(chunk) {
|
|
2748
|
-
const carried = this.
|
|
2823
|
+
const carried = this.pendingLength;
|
|
2749
2824
|
if (this.skipLeadingLF && chunk.length > 0) {
|
|
2750
2825
|
this.skipLeadingLF = false;
|
|
2751
2826
|
if (chunk.startsWith("\n")) chunk = chunk.slice(1);
|
|
2752
2827
|
}
|
|
2753
|
-
this.buffer += chunk;
|
|
2754
2828
|
let start2 = 0;
|
|
2755
|
-
for (let idx = 0; idx <
|
|
2756
|
-
const code =
|
|
2829
|
+
for (let idx = 0; idx < chunk.length; idx++) {
|
|
2830
|
+
const code = chunk.charCodeAt(idx);
|
|
2757
2831
|
if (code !== 10 && code !== 13) continue;
|
|
2758
|
-
|
|
2832
|
+
const head = chunk.slice(start2, idx);
|
|
2833
|
+
if (this.pending.length > 0) {
|
|
2834
|
+
this.pending.push(head);
|
|
2835
|
+
const line = this.pending.join("");
|
|
2836
|
+
this.pending = [];
|
|
2837
|
+
this.pendingLength = 0;
|
|
2838
|
+
this.processLine(line);
|
|
2839
|
+
} else {
|
|
2840
|
+
this.processLine(head);
|
|
2841
|
+
}
|
|
2759
2842
|
if (code === 13) {
|
|
2760
|
-
if (
|
|
2761
|
-
else if (idx + 1 ===
|
|
2843
|
+
if (chunk.charCodeAt(idx + 1) === 10) idx++;
|
|
2844
|
+
else if (idx + 1 === chunk.length) this.skipLeadingLF = true;
|
|
2762
2845
|
}
|
|
2763
2846
|
start2 = idx + 1;
|
|
2764
2847
|
}
|
|
2765
|
-
if (start2
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2848
|
+
if (start2 < chunk.length) {
|
|
2849
|
+
this.pending.push(start2 > 0 ? chunk.slice(start2) : chunk);
|
|
2850
|
+
this.pendingLength += chunk.length - start2;
|
|
2851
|
+
}
|
|
2852
|
+
if (this.pendingLength <= SSE_READER_MAX_BUFFER) return;
|
|
2853
|
+
if (start2 > 0 || carried === 0) {
|
|
2854
|
+
throw new ToolError2({
|
|
2769
2855
|
message: `SSE: chunk size ${chunk.length} exceeds max buffer ${SSE_READER_MAX_BUFFER} \u2014 refusing to accumulate`,
|
|
2770
2856
|
code: "TOOL_EXECUTION_FAILED",
|
|
2771
2857
|
toolName: "mcp_transport_sse_reader",
|
|
2772
2858
|
context: { phase: "feed", chunkLength: chunk.length, maxBuffer: SSE_READER_MAX_BUFFER }
|
|
2773
2859
|
});
|
|
2774
2860
|
}
|
|
2775
|
-
throw new
|
|
2861
|
+
throw new ToolError2({
|
|
2776
2862
|
message: `SSE: pending line exceeds ${SSE_READER_MAX_BUFFER} bytes \u2014 upstream is not framing events`,
|
|
2777
2863
|
code: "TOOL_EXECUTION_FAILED",
|
|
2778
2864
|
toolName: "mcp_transport_sse_reader",
|
|
2779
2865
|
context: {
|
|
2780
2866
|
phase: "feed",
|
|
2781
|
-
bufferLength: this.
|
|
2867
|
+
bufferLength: this.pendingLength,
|
|
2782
2868
|
maxBuffer: SSE_READER_MAX_BUFFER
|
|
2783
2869
|
}
|
|
2784
2870
|
});
|
|
2785
2871
|
}
|
|
2786
2872
|
processLine(line) {
|
|
2787
2873
|
if (line.length > SSE_READER_MAX_BUFFER) {
|
|
2788
|
-
throw new
|
|
2874
|
+
throw new ToolError2({
|
|
2789
2875
|
message: `SSE: completed line exceeds ${SSE_READER_MAX_BUFFER} characters`,
|
|
2790
2876
|
code: "TOOL_EXECUTION_FAILED",
|
|
2791
2877
|
toolName: "mcp_transport_sse_reader"
|
|
@@ -2805,14 +2891,14 @@ var SSEReader = class {
|
|
|
2805
2891
|
} else if (field === "data") {
|
|
2806
2892
|
const nextLength = this.dataLength + value.length + (this.dataLines.length > 0 ? 1 : 0);
|
|
2807
2893
|
if (nextLength > SSE_READER_MAX_BUFFER) {
|
|
2808
|
-
throw new
|
|
2894
|
+
throw new ToolError2({
|
|
2809
2895
|
message: `SSE: event data exceeds ${SSE_READER_MAX_BUFFER} characters`,
|
|
2810
2896
|
code: "TOOL_EXECUTION_FAILED",
|
|
2811
2897
|
toolName: "mcp_transport_sse_reader"
|
|
2812
2898
|
});
|
|
2813
2899
|
}
|
|
2814
2900
|
if (this.dataLines.length >= SSE_READER_MAX_DATA_LINES) {
|
|
2815
|
-
throw new
|
|
2901
|
+
throw new ToolError2({
|
|
2816
2902
|
message: `SSE: exceeded ${SSE_READER_MAX_DATA_LINES} data lines per event \u2014 upstream is not sending blank-line delimiters`,
|
|
2817
2903
|
code: "TOOL_EXECUTION_FAILED",
|
|
2818
2904
|
toolName: "mcp_transport_sse_reader",
|
|
@@ -2838,7 +2924,10 @@ var SSEReader = class {
|
|
|
2838
2924
|
this.dataLines = [];
|
|
2839
2925
|
if (!data) return;
|
|
2840
2926
|
if (eventName === "endpoint") {
|
|
2841
|
-
|
|
2927
|
+
const listeners = this.endpointListeners;
|
|
2928
|
+
for (const cb of [...listeners]) {
|
|
2929
|
+
if (listeners !== this.endpointListeners) break;
|
|
2930
|
+
if (!listeners.includes(cb)) continue;
|
|
2842
2931
|
try {
|
|
2843
2932
|
cb(data);
|
|
2844
2933
|
} catch {
|
|
@@ -2853,7 +2942,10 @@ var SSEReader = class {
|
|
|
2853
2942
|
}
|
|
2854
2943
|
}
|
|
2855
2944
|
dispatch(msg) {
|
|
2856
|
-
|
|
2945
|
+
const listeners = this.listeners;
|
|
2946
|
+
for (const cb of [...listeners]) {
|
|
2947
|
+
if (listeners !== this.listeners) break;
|
|
2948
|
+
if (!listeners.includes(cb)) continue;
|
|
2857
2949
|
try {
|
|
2858
2950
|
cb(msg);
|
|
2859
2951
|
} catch {
|
|
@@ -2861,7 +2953,8 @@ var SSEReader = class {
|
|
|
2861
2953
|
}
|
|
2862
2954
|
}
|
|
2863
2955
|
reset() {
|
|
2864
|
-
this.
|
|
2956
|
+
this.pending = [];
|
|
2957
|
+
this.pendingLength = 0;
|
|
2865
2958
|
this.skipLeadingLF = false;
|
|
2866
2959
|
this.dataLines = [];
|
|
2867
2960
|
this.eventName = "";
|
|
@@ -2875,51 +2968,6 @@ var SSEReader = class {
|
|
|
2875
2968
|
import * as https2 from "node:https";
|
|
2876
2969
|
import { ConfigError as ConfigError2 } from "@wrongstack/core/types";
|
|
2877
2970
|
|
|
2878
|
-
// src/read-body.ts
|
|
2879
|
-
import { ToolError as ToolError2 } from "@wrongstack/core/types";
|
|
2880
|
-
var MAX_MCP_HTTP_BODY_BYTES = 16 * 1024 * 1024;
|
|
2881
|
-
async function readBodyCapped(res, maxBytes = MAX_MCP_HTTP_BODY_BYTES) {
|
|
2882
|
-
const safeCap = typeof maxBytes === "number" && Number.isFinite(maxBytes) && maxBytes >= 0 ? maxBytes : MAX_MCP_HTTP_BODY_BYTES;
|
|
2883
|
-
const body = res.body;
|
|
2884
|
-
if (!body || typeof body.getReader !== "function") {
|
|
2885
|
-
const text = await res.text();
|
|
2886
|
-
const received = Buffer.byteLength(text, "utf8");
|
|
2887
|
-
if (received > safeCap) {
|
|
2888
|
-
throw new ToolError2({
|
|
2889
|
-
message: `MCP response body exceeded ${safeCap} bytes \u2014 refusing to buffer`,
|
|
2890
|
-
code: "TOOL_EXECUTION_FAILED",
|
|
2891
|
-
toolName: "mcp_transport",
|
|
2892
|
-
context: { maxBytes: safeCap, received }
|
|
2893
|
-
});
|
|
2894
|
-
}
|
|
2895
|
-
return text;
|
|
2896
|
-
}
|
|
2897
|
-
const reader = body.getReader();
|
|
2898
|
-
const chunks = [];
|
|
2899
|
-
let total = 0;
|
|
2900
|
-
try {
|
|
2901
|
-
for (; ; ) {
|
|
2902
|
-
const { done, value } = await reader.read();
|
|
2903
|
-
if (done) break;
|
|
2904
|
-
if (!value) continue;
|
|
2905
|
-
total += value.byteLength;
|
|
2906
|
-
if (total > safeCap) {
|
|
2907
|
-
await reader.cancel().catch(() => void 0);
|
|
2908
|
-
throw new ToolError2({
|
|
2909
|
-
message: `MCP response body exceeded ${safeCap} bytes \u2014 refusing to buffer`,
|
|
2910
|
-
code: "TOOL_EXECUTION_FAILED",
|
|
2911
|
-
toolName: "mcp_transport",
|
|
2912
|
-
context: { maxBytes: safeCap, received: total }
|
|
2913
|
-
});
|
|
2914
|
-
}
|
|
2915
|
-
chunks.push(value);
|
|
2916
|
-
}
|
|
2917
|
-
} finally {
|
|
2918
|
-
reader.releaseLock?.();
|
|
2919
|
-
}
|
|
2920
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
2921
|
-
}
|
|
2922
|
-
|
|
2923
2971
|
// src/transport-security.ts
|
|
2924
2972
|
import * as dns2 from "node:dns/promises";
|
|
2925
2973
|
import * as net2 from "node:net";
|
|
@@ -3175,8 +3223,8 @@ var BaseHTTPTransport = class {
|
|
|
3175
3223
|
this.headers = { ...opts.headers };
|
|
3176
3224
|
this.authorizationProvider = opts.authorizationProvider;
|
|
3177
3225
|
this.authorizationResource = canonicalMcpResource(opts.url);
|
|
3178
|
-
this.timeout = typeof opts.startupTimeoutMs === "number" && Number.isFinite(opts.startupTimeoutMs) && opts.startupTimeoutMs > 0 ? opts.startupTimeoutMs : 1e4;
|
|
3179
|
-
this.requestTimeout = typeof opts.requestTimeoutMs === "number" && Number.isFinite(opts.requestTimeoutMs) && opts.requestTimeoutMs > 0 ? opts.requestTimeoutMs : MCP_CONSTANTS.REQUEST_TIMEOUT_MS;
|
|
3226
|
+
this.timeout = typeof opts.startupTimeoutMs === "number" && Number.isFinite(opts.startupTimeoutMs) && opts.startupTimeoutMs > 0 ? Math.min(opts.startupTimeoutMs, 2147483647) : 1e4;
|
|
3227
|
+
this.requestTimeout = typeof opts.requestTimeoutMs === "number" && Number.isFinite(opts.requestTimeoutMs) && opts.requestTimeoutMs > 0 ? Math.min(opts.requestTimeoutMs, 2147483647) : MCP_CONSTANTS.REQUEST_TIMEOUT_MS;
|
|
3180
3228
|
if (opts.tls) {
|
|
3181
3229
|
if (opts.tls.rejectUnauthorized === false) {
|
|
3182
3230
|
const where = urlOriginForLog(this.url);
|
|
@@ -3480,6 +3528,14 @@ function isJsonRpcMethodEnvelope(v) {
|
|
|
3480
3528
|
}
|
|
3481
3529
|
function extractJsonRpcEnvelopes(text) {
|
|
3482
3530
|
const out = [];
|
|
3531
|
+
const whole = text.trim();
|
|
3532
|
+
if (whole.startsWith("{") && /[\r\n]/.test(whole)) {
|
|
3533
|
+
try {
|
|
3534
|
+
const parsed = JSON.parse(whole);
|
|
3535
|
+
if (isJsonRpcResult(parsed) || isJsonRpcMethodEnvelope(parsed)) return [parsed];
|
|
3536
|
+
} catch {
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3483
3539
|
let dataBuf = [];
|
|
3484
3540
|
const flush = () => {
|
|
3485
3541
|
if (dataBuf.length === 0) return;
|
|
@@ -3579,6 +3635,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
3579
3635
|
this.tools.splice(0, this.tools.length, ...tools);
|
|
3580
3636
|
this.toolCatalogRevision++;
|
|
3581
3637
|
for (const cb of this.toolsChangedListeners) {
|
|
3638
|
+
if (version !== this.toolCatalogVersion) break;
|
|
3582
3639
|
try {
|
|
3583
3640
|
cb([...this.tools]);
|
|
3584
3641
|
} catch {
|
|
@@ -4091,6 +4148,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
4091
4148
|
this.tools.splice(0, this.tools.length, ...tools);
|
|
4092
4149
|
this.toolCatalogRevision++;
|
|
4093
4150
|
for (const listener of this.toolsChangedListeners) {
|
|
4151
|
+
if (version !== this.toolCatalogVersion) break;
|
|
4094
4152
|
try {
|
|
4095
4153
|
listener([...tools]);
|
|
4096
4154
|
} catch {
|
|
@@ -4365,9 +4423,11 @@ async function connectSSE(host) {
|
|
|
4365
4423
|
}
|
|
4366
4424
|
});
|
|
4367
4425
|
host.sseTransport.onToolsChanged((tools) => {
|
|
4426
|
+
const version = host.toolCatalogVersion;
|
|
4368
4427
|
host._tools = tools;
|
|
4369
4428
|
host._toolsCache = tools;
|
|
4370
4429
|
for (const cb of host.toolsChangedListeners) {
|
|
4430
|
+
if (version !== host.toolCatalogVersion) break;
|
|
4371
4431
|
try {
|
|
4372
4432
|
cb(host.opts.name, tools);
|
|
4373
4433
|
} catch {
|
|
@@ -4420,9 +4480,11 @@ async function connectStreamableHTTP(host) {
|
|
|
4420
4480
|
}
|
|
4421
4481
|
});
|
|
4422
4482
|
host.httpTransport.onToolsChanged((tools) => {
|
|
4483
|
+
const version = host.toolCatalogVersion;
|
|
4423
4484
|
host._tools = tools;
|
|
4424
4485
|
host._toolsCache = tools;
|
|
4425
4486
|
for (const cb of host.toolsChangedListeners) {
|
|
4487
|
+
if (version !== host.toolCatalogVersion) break;
|
|
4426
4488
|
try {
|
|
4427
4489
|
cb(host.opts.name, tools);
|
|
4428
4490
|
} catch {
|
|
@@ -4533,7 +4595,7 @@ async function connectStdio(host) {
|
|
|
4533
4595
|
capabilities: host.serverRequests.capabilities(),
|
|
4534
4596
|
clientInfo: MCP_CONSTANTS.CLIENT_INFO
|
|
4535
4597
|
},
|
|
4536
|
-
typeof host.opts.startupTimeoutMs === "number" && Number.isFinite(host.opts.startupTimeoutMs) && host.opts.startupTimeoutMs > 0 ? host.opts.startupTimeoutMs : 1e4
|
|
4598
|
+
typeof host.opts.startupTimeoutMs === "number" && Number.isFinite(host.opts.startupTimeoutMs) && host.opts.startupTimeoutMs > 0 ? Math.min(host.opts.startupTimeoutMs, 2147483647) : 1e4
|
|
4537
4599
|
);
|
|
4538
4600
|
if (initialize.error) {
|
|
4539
4601
|
host.state = "failed";
|
|
@@ -4588,6 +4650,7 @@ async function notifyStdio(host, method, params) {
|
|
|
4588
4650
|
const timeout = setTimeout(() => {
|
|
4589
4651
|
stdin.removeListener?.("drain", onDrain);
|
|
4590
4652
|
stdin.removeListener?.("error", onError);
|
|
4653
|
+
stdin.removeListener?.("close", onClose);
|
|
4591
4654
|
host._drainPending = false;
|
|
4592
4655
|
reject(new Error(`MCP notify("${method}") drain timeout`));
|
|
4593
4656
|
}, 500);
|
|
@@ -4595,6 +4658,7 @@ async function notifyStdio(host, method, params) {
|
|
|
4595
4658
|
clearTimeout(timeout);
|
|
4596
4659
|
stdin.removeListener?.("drain", onDrain);
|
|
4597
4660
|
stdin.removeListener?.("error", onError);
|
|
4661
|
+
stdin.removeListener?.("close", onClose);
|
|
4598
4662
|
host._drainPending = false;
|
|
4599
4663
|
resolve();
|
|
4600
4664
|
};
|
|
@@ -4602,11 +4666,14 @@ async function notifyStdio(host, method, params) {
|
|
|
4602
4666
|
clearTimeout(timeout);
|
|
4603
4667
|
stdin.removeListener?.("drain", onDrain);
|
|
4604
4668
|
stdin.removeListener?.("error", onError);
|
|
4669
|
+
stdin.removeListener?.("close", onClose);
|
|
4605
4670
|
host._drainPending = false;
|
|
4606
4671
|
reject(err);
|
|
4607
4672
|
};
|
|
4673
|
+
const onClose = () => onError(new Error(`MCP notify("${method}") stdin closed`));
|
|
4608
4674
|
stdin.once?.("drain", onDrain);
|
|
4609
4675
|
stdin.once?.("error", onError);
|
|
4676
|
+
stdin.once?.("close", onClose);
|
|
4610
4677
|
});
|
|
4611
4678
|
}
|
|
4612
4679
|
} catch (err) {
|
|
@@ -4700,7 +4767,10 @@ function receiveStdioLine(host, line) {
|
|
|
4700
4767
|
// src/client-stdio-request.ts
|
|
4701
4768
|
function resolveRequestTimeoutMs(configuredMs, timeoutMs) {
|
|
4702
4769
|
const defaultTimeoutMs = typeof configuredMs === "number" && Number.isFinite(configuredMs) && configuredMs > 0 ? configuredMs : MCP_CONSTANTS.REQUEST_TIMEOUT_MS;
|
|
4703
|
-
return
|
|
4770
|
+
return Math.min(
|
|
4771
|
+
typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : defaultTimeoutMs,
|
|
4772
|
+
2147483647
|
|
4773
|
+
);
|
|
4704
4774
|
}
|
|
4705
4775
|
function sendStdioRequest(self, method, params, effectiveTimeoutMs, opts) {
|
|
4706
4776
|
const signal = opts?.signal;
|
|
@@ -4994,6 +5064,31 @@ var MCPClient = class {
|
|
|
4994
5064
|
async getPrompt(name, args, opts = {}) {
|
|
4995
5065
|
return this.capabilityClient.getPrompt(name, args, opts);
|
|
4996
5066
|
}
|
|
5067
|
+
/**
|
|
5068
|
+
* Liveness check (MCP `ping`): the server MUST answer promptly with an
|
|
5069
|
+
* empty result. Works on every transport — a cheap connection health probe
|
|
5070
|
+
* before a long call, and the natural reconnect-readiness check.
|
|
5071
|
+
*/
|
|
5072
|
+
async ping(opts = {}) {
|
|
5073
|
+
const res = await this.request("ping", {}, void 0, opts);
|
|
5074
|
+
if (res.error) {
|
|
5075
|
+
throw new Error(`MCP ping failed: ${res.error.message}`);
|
|
5076
|
+
}
|
|
5077
|
+
parseEmptyResult(res.result);
|
|
5078
|
+
}
|
|
5079
|
+
/**
|
|
5080
|
+
* Argument autocompletion (MCP `completion/complete`, 2024-11-05). The
|
|
5081
|
+
* server must advertise the `completions` capability; an unsupported
|
|
5082
|
+
* server answers with a JSON-RPC error, which surfaces here as a throw.
|
|
5083
|
+
*/
|
|
5084
|
+
async complete(ref, argument, opts = {}) {
|
|
5085
|
+
validateProtocolString(argument.name, "completion argument name");
|
|
5086
|
+
const res = await this.request("completion/complete", { ref, argument }, void 0, opts);
|
|
5087
|
+
if (res.error) {
|
|
5088
|
+
throw new Error(`MCP completion/complete failed: ${res.error.message}`);
|
|
5089
|
+
}
|
|
5090
|
+
return parseCompletionResult(res.result);
|
|
5091
|
+
}
|
|
4997
5092
|
async close() {
|
|
4998
5093
|
if (this.closePromise) return this.closePromise;
|
|
4999
5094
|
this.closePromise = this.closeInner().finally(() => {
|
|
@@ -5154,6 +5249,7 @@ function prepareResourceInsertion(serverName, requestedUri, result, policy = {})
|
|
|
5154
5249
|
throw new Error("MCP resource insertion exceeds the limit of 64 content blocks");
|
|
5155
5250
|
}
|
|
5156
5251
|
let byteSize = 0;
|
|
5252
|
+
enforceSize(byteSize, policy);
|
|
5157
5253
|
for (const content of result.contents) {
|
|
5158
5254
|
validateUri(content.uri, policy);
|
|
5159
5255
|
if (content.text !== void 0) byteSize += utf8Bytes(content.text);
|
|
@@ -5497,7 +5593,7 @@ async function readConfig(path2) {
|
|
|
5497
5593
|
}
|
|
5498
5594
|
let parsed;
|
|
5499
5595
|
try {
|
|
5500
|
-
parsed = JSON.parse(raw);
|
|
5596
|
+
parsed = JSON.parse(raw.replace(/^\uFEFF/, ""));
|
|
5501
5597
|
} catch {
|
|
5502
5598
|
throw new Error(
|
|
5503
5599
|
`Profile config "${path2}" is not valid JSON \u2014 fix or remove it before changing MCP servers`
|
|
@@ -5841,6 +5937,7 @@ function manifestConfigHash(cfg) {
|
|
|
5841
5937
|
env: sortedEntries(cfg.env),
|
|
5842
5938
|
headers: sortedEntries(cfg.headers),
|
|
5843
5939
|
bearerTokenEnv: cfg.bearerTokenEnv ?? null,
|
|
5940
|
+
...cfg.bearerTokenEnv ? { bearerToken: process.env[cfg.bearerTokenEnv.trim()] ?? null } : {},
|
|
5844
5941
|
passthroughEnv: passthroughEntries(cfg.passthroughEnv)
|
|
5845
5942
|
});
|
|
5846
5943
|
return createHash2("sha256").update(basis).digest("hex").slice(0, 16);
|
|
@@ -6254,7 +6351,7 @@ function describeSlotTools(slot) {
|
|
|
6254
6351
|
// Behaviour hints the SERVER claimed (already sanitized to the five
|
|
6255
6352
|
// known keys by normalizeMCPTools). Surfaced for the operator only:
|
|
6256
6353
|
// nothing in the permission path reads this field.
|
|
6257
|
-
...tool.annotations ? { annotations: tool.annotations } : {}
|
|
6354
|
+
...tool.annotations ? { annotations: structuredClone(tool.annotations) } : {}
|
|
6258
6355
|
}));
|
|
6259
6356
|
}
|
|
6260
6357
|
function listServers(servers) {
|
|
@@ -7289,8 +7386,11 @@ function detachCatalogListeners(self, client) {
|
|
|
7289
7386
|
function handleChildExit(self, name, code) {
|
|
7290
7387
|
const slot = self.servers.get(name);
|
|
7291
7388
|
if (slot?.state !== "connected") return;
|
|
7389
|
+
const generation = slot.startupGeneration;
|
|
7390
|
+
const isCurrent = () => self.servers.get(name) === slot && slot.startupGeneration === generation;
|
|
7292
7391
|
if (slot.lazy) {
|
|
7293
7392
|
self.recordFailure(slot, "transport", "process-exit-lazy");
|
|
7393
|
+
if (!isCurrent()) return;
|
|
7294
7394
|
markLazySlotDormant(slot, self.events, `exit:${code ?? "unknown"}`, {
|
|
7295
7395
|
onChildExit: self.onChildExit,
|
|
7296
7396
|
onToolsChanged: self.onToolsChanged,
|
|
@@ -7301,14 +7401,18 @@ function handleChildExit(self, name, code) {
|
|
|
7301
7401
|
resetDisconnectedSlotTools(slot, self.toolRegistry);
|
|
7302
7402
|
slot.state = "disconnected";
|
|
7303
7403
|
self.recordFailure(slot, "transport", "process-exit");
|
|
7404
|
+
if (!isCurrent()) return;
|
|
7304
7405
|
self.events.emit("mcp.server.disconnected", { name, reason: `exit:${code ?? "unknown"}` });
|
|
7305
|
-
self.scheduleReconnect(slot);
|
|
7406
|
+
if (isCurrent()) self.scheduleReconnect(slot);
|
|
7306
7407
|
}
|
|
7307
7408
|
function handleTransportDisconnect(self, name) {
|
|
7308
7409
|
const slot = self.servers.get(name);
|
|
7309
7410
|
if (slot?.state !== "connected") return;
|
|
7411
|
+
const generation = slot.startupGeneration;
|
|
7412
|
+
const isCurrent = () => self.servers.get(name) === slot && slot.startupGeneration === generation;
|
|
7310
7413
|
if (slot.lazy) {
|
|
7311
7414
|
self.recordFailure(slot, "transport", "http-disconnect-lazy");
|
|
7415
|
+
if (!isCurrent()) return;
|
|
7312
7416
|
markLazySlotDormant(slot, self.events, "http-disconnect", {
|
|
7313
7417
|
onChildExit: self.onChildExit,
|
|
7314
7418
|
onToolsChanged: self.onToolsChanged,
|
|
@@ -7319,8 +7423,9 @@ function handleTransportDisconnect(self, name) {
|
|
|
7319
7423
|
resetDisconnectedSlotTools(slot, self.toolRegistry);
|
|
7320
7424
|
slot.state = "disconnected";
|
|
7321
7425
|
self.recordFailure(slot, "transport", "http-disconnect");
|
|
7426
|
+
if (!isCurrent()) return;
|
|
7322
7427
|
self.events.emit("mcp.server.disconnected", { name, reason: "http-disconnect" });
|
|
7323
|
-
self.scheduleReconnect(slot);
|
|
7428
|
+
if (isCurrent()) self.scheduleReconnect(slot);
|
|
7324
7429
|
}
|
|
7325
7430
|
var MAX_RECONNECT_CYCLES = MCP_CONSTANTS.RECONNECT.MAX_CYCLES;
|
|
7326
7431
|
var BASE_RECONNECT_DELAY_MS = MCP_CONSTANTS.RECONNECT.BASE_DELAY_MS;
|
|
@@ -7793,6 +7898,8 @@ function serveStdio(server, opts = {}) {
|
|
|
7793
7898
|
if (closed) return;
|
|
7794
7899
|
closed = true;
|
|
7795
7900
|
stdin.off("data", onData);
|
|
7901
|
+
stdin.off("end", onEnd);
|
|
7902
|
+
stdin.off("close", onEnd);
|
|
7796
7903
|
if (!bufferTooLarge && buffer.trim()) {
|
|
7797
7904
|
const line = buffer.trim();
|
|
7798
7905
|
buffer = "";
|
|
@@ -8162,6 +8269,7 @@ var MCPRefreshingAuthorizationProvider = class {
|
|
|
8162
8269
|
}
|
|
8163
8270
|
throw err;
|
|
8164
8271
|
}
|
|
8272
|
+
if (!tokenSet.scopes?.length) tokenSet = { ...tokenSet, scopes: state.tokenSet.scopes };
|
|
8165
8273
|
const next = normalizeStoredAuthorization({
|
|
8166
8274
|
...state,
|
|
8167
8275
|
tokenSet,
|
|
@@ -8327,6 +8435,7 @@ export {
|
|
|
8327
8435
|
mcpEnvPlaceholders,
|
|
8328
8436
|
negotiateProtocolVersion,
|
|
8329
8437
|
parseAuthorizationServerMetadata,
|
|
8438
|
+
parseCompletionResult,
|
|
8330
8439
|
parseGetPromptResult,
|
|
8331
8440
|
parseListPromptsResult,
|
|
8332
8441
|
parseListResourceTemplatesResult,
|
package/dist/protocol.d.ts
CHANGED
|
@@ -139,4 +139,28 @@ export declare function progressNotification(params: unknown): MCPProgressNotifi
|
|
|
139
139
|
* notification has no reply.
|
|
140
140
|
*/
|
|
141
141
|
export declare function logMessageNotification(params: unknown): MCPLogMessageNotification | undefined;
|
|
142
|
+
/** Reference identifying what to autocomplete (MCP `Reference`). */
|
|
143
|
+
export type MCPCompletionReference = {
|
|
144
|
+
type: 'ref/prompt';
|
|
145
|
+
name: string;
|
|
146
|
+
} | {
|
|
147
|
+
type: 'ref/resource';
|
|
148
|
+
uri: string;
|
|
149
|
+
};
|
|
150
|
+
export interface MCPCompletionResult {
|
|
151
|
+
completion: {
|
|
152
|
+
/** Suggested completions, most relevant first. May be empty. */
|
|
153
|
+
values: string[];
|
|
154
|
+
/** Total completions the server knows about, when it reports it. */
|
|
155
|
+
total?: number | undefined;
|
|
156
|
+
/** True when more completions exist beyond `values`. */
|
|
157
|
+
hasMore?: boolean | undefined;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Parse a `completion/complete` result (2024-11-05 completions utility).
|
|
162
|
+
* `values` is required (may be empty); `total`/`hasMore` are validated only
|
|
163
|
+
* when present, so a malformed optional cannot masquerade as valid pagination.
|
|
164
|
+
*/
|
|
165
|
+
export declare function parseCompletionResult(value: unknown): MCPCompletionResult;
|
|
142
166
|
//# sourceMappingURL=protocol.d.ts.map
|
package/dist/sse-reader.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.36",
|
|
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.36",
|
|
30
30
|
"undici": "^8.10.2"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "node ../../scripts/build-package.mjs",
|
|
43
|
-
"typecheck": "
|
|
43
|
+
"typecheck": "node ../../scripts/bun-typecheck.mjs --noEmit -p tsconfig.test.json",
|
|
44
44
|
"test": "vitest run",
|
|
45
45
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
46
46
|
}
|