@wrongstack/mcp 1.0.35 → 1.0.37

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.
@@ -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 { MCPGetPromptResult, MCPListPromptsResult, MCPListResourcesResult, MCPListResourceTemplatesResult, MCPReadResourceResult, MCPServerMetadata } from './protocol.js';
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
- if (field.minLength !== void 0 && raw.length < field.minLength) {
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 && raw.length > field.maxLength) {
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 SSE_READER_MAX_BUFFER = 256 * 1024;
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
- buffer = "";
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.buffer.length;
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 < this.buffer.length; idx++) {
2756
- const code = this.buffer.charCodeAt(idx);
2829
+ for (let idx = 0; idx < chunk.length; idx++) {
2830
+ const code = chunk.charCodeAt(idx);
2757
2831
  if (code !== 10 && code !== 13) continue;
2758
- this.processLine(this.buffer.slice(start2, idx));
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 (this.buffer.charCodeAt(idx + 1) === 10) idx++;
2761
- else if (idx + 1 === this.buffer.length) this.skipLeadingLF = true;
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 > 0) this.buffer = this.buffer.slice(start2);
2766
- if (this.buffer.length <= SSE_READER_MAX_BUFFER) return;
2767
- if (start2 >= carried) {
2768
- throw new ToolError({
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 ToolError({
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.buffer.length,
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 ToolError({
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 ToolError({
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 ToolError({
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
- for (const cb of this.endpointListeners) {
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
- for (const cb of this.listeners) {
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.buffer = "";
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";
@@ -3083,7 +3131,7 @@ var undiciModule;
3083
3131
  var undiciLoad;
3084
3132
  async function ensureUndici() {
3085
3133
  if (undiciModule) return undiciModule;
3086
- undiciLoad ??= import("undici");
3134
+ undiciLoad ??= import("undici/index.js");
3087
3135
  undiciModule = await undiciLoad;
3088
3136
  return undiciModule;
3089
3137
  }
@@ -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 typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : defaultTimeoutMs;
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) {
@@ -6558,7 +6655,11 @@ function applySlotTools(ctx, slot, tools, client) {
6558
6655
  if (at >= 0) slot.activeCallers?.splice(at, 1);
6559
6656
  slot.operations.inFlightCalls = Math.max(0, slot.operations.inFlightCalls - 1);
6560
6657
  slot.lastUsed = Date.now();
6561
- pushBounded(slot.operations.callSamples, durationMs, MCP_OPERATION_LIMITS.LATENCY_SAMPLES);
6658
+ pushBounded(
6659
+ slot.operations.callSamples,
6660
+ durationMs,
6661
+ MCP_OPERATION_LIMITS.LATENCY_SAMPLES
6662
+ );
6562
6663
  if (ok) {
6563
6664
  ctx.recordSuccess(slot);
6564
6665
  ctx.recordOperation(slot, "call", "ok", void 0, durationMs, false);
@@ -7289,8 +7390,11 @@ function detachCatalogListeners(self, client) {
7289
7390
  function handleChildExit(self, name, code) {
7290
7391
  const slot = self.servers.get(name);
7291
7392
  if (slot?.state !== "connected") return;
7393
+ const generation = slot.startupGeneration;
7394
+ const isCurrent = () => self.servers.get(name) === slot && slot.startupGeneration === generation;
7292
7395
  if (slot.lazy) {
7293
7396
  self.recordFailure(slot, "transport", "process-exit-lazy");
7397
+ if (!isCurrent()) return;
7294
7398
  markLazySlotDormant(slot, self.events, `exit:${code ?? "unknown"}`, {
7295
7399
  onChildExit: self.onChildExit,
7296
7400
  onToolsChanged: self.onToolsChanged,
@@ -7301,14 +7405,18 @@ function handleChildExit(self, name, code) {
7301
7405
  resetDisconnectedSlotTools(slot, self.toolRegistry);
7302
7406
  slot.state = "disconnected";
7303
7407
  self.recordFailure(slot, "transport", "process-exit");
7408
+ if (!isCurrent()) return;
7304
7409
  self.events.emit("mcp.server.disconnected", { name, reason: `exit:${code ?? "unknown"}` });
7305
- self.scheduleReconnect(slot);
7410
+ if (isCurrent()) self.scheduleReconnect(slot);
7306
7411
  }
7307
7412
  function handleTransportDisconnect(self, name) {
7308
7413
  const slot = self.servers.get(name);
7309
7414
  if (slot?.state !== "connected") return;
7415
+ const generation = slot.startupGeneration;
7416
+ const isCurrent = () => self.servers.get(name) === slot && slot.startupGeneration === generation;
7310
7417
  if (slot.lazy) {
7311
7418
  self.recordFailure(slot, "transport", "http-disconnect-lazy");
7419
+ if (!isCurrent()) return;
7312
7420
  markLazySlotDormant(slot, self.events, "http-disconnect", {
7313
7421
  onChildExit: self.onChildExit,
7314
7422
  onToolsChanged: self.onToolsChanged,
@@ -7319,8 +7427,9 @@ function handleTransportDisconnect(self, name) {
7319
7427
  resetDisconnectedSlotTools(slot, self.toolRegistry);
7320
7428
  slot.state = "disconnected";
7321
7429
  self.recordFailure(slot, "transport", "http-disconnect");
7430
+ if (!isCurrent()) return;
7322
7431
  self.events.emit("mcp.server.disconnected", { name, reason: "http-disconnect" });
7323
- self.scheduleReconnect(slot);
7432
+ if (isCurrent()) self.scheduleReconnect(slot);
7324
7433
  }
7325
7434
  var MAX_RECONNECT_CYCLES = MCP_CONSTANTS.RECONNECT.MAX_CYCLES;
7326
7435
  var BASE_RECONNECT_DELAY_MS = MCP_CONSTANTS.RECONNECT.BASE_DELAY_MS;
@@ -7793,6 +7902,8 @@ function serveStdio(server, opts = {}) {
7793
7902
  if (closed) return;
7794
7903
  closed = true;
7795
7904
  stdin.off("data", onData);
7905
+ stdin.off("end", onEnd);
7906
+ stdin.off("close", onEnd);
7796
7907
  if (!bufferTooLarge && buffer.trim()) {
7797
7908
  const line = buffer.trim();
7798
7909
  buffer = "";
@@ -8162,6 +8273,7 @@ var MCPRefreshingAuthorizationProvider = class {
8162
8273
  }
8163
8274
  throw err;
8164
8275
  }
8276
+ if (!tokenSet.scopes?.length) tokenSet = { ...tokenSet, scopes: state.tokenSet.scopes };
8165
8277
  const next = normalizeStoredAuthorization({
8166
8278
  ...state,
8167
8279
  tokenSet,
@@ -8327,6 +8439,7 @@ export {
8327
8439
  mcpEnvPlaceholders,
8328
8440
  negotiateProtocolVersion,
8329
8441
  parseAuthorizationServerMetadata,
8442
+ parseCompletionResult,
8330
8443
  parseGetPromptResult,
8331
8444
  parseListPromptsResult,
8332
8445
  parseListResourceTemplatesResult,
@@ -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
@@ -1,5 +1,6 @@
1
1
  export declare class SSEReader {
2
- private buffer;
2
+ private pending;
3
+ private pendingLength;
3
4
  private skipLeadingLF;
4
5
  private dataLines;
5
6
  private dataLength;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/mcp",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack Model Context Protocol client and registry: stdio, SSE, and streamable HTTP transports.",
6
6
  "repository": {
@@ -25,8 +25,14 @@
25
25
  "dist",
26
26
  "!dist/**/*.map"
27
27
  ],
28
+ "scripts": {
29
+ "build": "bun ../../scripts/build-package.mjs",
30
+ "typecheck": "bun ../../scripts/bun-typecheck.mjs --noEmit -p tsconfig.test.json",
31
+ "test": "bun --bun run vitest run",
32
+ "clean": "bun -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
33
+ },
28
34
  "dependencies": {
29
- "@wrongstack/core": "1.0.35",
35
+ "@wrongstack/core": "1.0.37",
30
36
  "undici": "^8.10.2"
31
37
  },
32
38
  "devDependencies": {
@@ -37,11 +43,5 @@
37
43
  },
38
44
  "publishConfig": {
39
45
  "access": "public"
40
- },
41
- "scripts": {
42
- "build": "node ../../scripts/build-package.mjs",
43
- "typecheck": "tsc --noEmit -p tsconfig.test.json",
44
- "test": "vitest run",
45
- "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
46
46
  }
47
- }
47
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 ECOSTACK TECHNOLOGY OÜ
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.