@mcp-ts/sdk 2.4.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +58 -19
  2. package/dist/adapters/agui-adapter.d.mts +2 -2
  3. package/dist/adapters/agui-adapter.d.ts +2 -2
  4. package/dist/adapters/agui-middleware.d.mts +2 -2
  5. package/dist/adapters/agui-middleware.d.ts +2 -2
  6. package/dist/adapters/ai-adapter.d.mts +2 -2
  7. package/dist/adapters/ai-adapter.d.ts +2 -2
  8. package/dist/adapters/langchain-adapter.d.mts +2 -2
  9. package/dist/adapters/langchain-adapter.d.ts +2 -2
  10. package/dist/adapters/mastra-adapter.d.mts +2 -2
  11. package/dist/adapters/mastra-adapter.d.ts +2 -2
  12. package/dist/client/index.d.mts +2 -2
  13. package/dist/client/index.d.ts +2 -2
  14. package/dist/client/react.d.mts +4 -4
  15. package/dist/client/react.d.ts +4 -4
  16. package/dist/client/vue.d.mts +4 -4
  17. package/dist/client/vue.d.ts +4 -4
  18. package/dist/{events-CK3N--3g.d.mts → events-C4m7tK1U.d.mts} +0 -1
  19. package/dist/{events-CK3N--3g.d.ts → events-C4m7tK1U.d.ts} +0 -1
  20. package/dist/{index-CtXvKl8N.d.ts → index-Ch7ouNSa.d.ts} +1 -1
  21. package/dist/{index-ByIjEReo.d.mts → index-L5XoXgsb.d.mts} +1 -1
  22. package/dist/index.d.mts +3 -3
  23. package/dist/index.d.ts +3 -3
  24. package/dist/index.js +71 -20
  25. package/dist/index.js.map +1 -1
  26. package/dist/index.mjs +71 -20
  27. package/dist/index.mjs.map +1 -1
  28. package/dist/{multi-session-client-CnvZEGPY.d.ts → multi-session-client-Bwm-efqg.d.ts} +28 -5
  29. package/dist/{multi-session-client-CIMUGF8S.d.mts → multi-session-client-k-9RvWvi.d.mts} +28 -5
  30. package/dist/server/index.d.mts +4 -4
  31. package/dist/server/index.d.ts +4 -4
  32. package/dist/server/index.js +71 -20
  33. package/dist/server/index.js.map +1 -1
  34. package/dist/server/index.mjs +71 -20
  35. package/dist/server/index.mjs.map +1 -1
  36. package/dist/shared/index.d.mts +1 -1
  37. package/dist/shared/index.d.ts +1 -1
  38. package/dist/shared/index.js.map +1 -1
  39. package/dist/shared/index.mjs.map +1 -1
  40. package/migrations/v2.3.4/neon/20260513010000_install_mcp_sessions.sql +69 -0
  41. package/migrations/v2.3.4/neon/20260513020000_add_session_cleanup_cron.sql +35 -0
  42. package/migrations/v2.3.4/supabase/20260330195700_install_mcp_sessions.sql +82 -0
  43. package/migrations/v2.3.4/supabase/20260421010000_add_session_cleanup_cron.sql +32 -0
  44. package/package.json +1 -1
  45. package/src/server/handlers/sse-handler.ts +7 -5
  46. package/src/server/mcp/multi-session-client.ts +46 -5
  47. package/src/server/mcp/oauth-client.ts +60 -10
  48. package/src/shared/events.ts +0 -1
@@ -2218,7 +2218,7 @@ var MCPClient = class {
2218
2218
  * Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
2219
2219
  * This wrapper enforces a timeout and properly uses AbortController to unblock the request.
2220
2220
  */
2221
- fetch: (url, init) => {
2221
+ fetch: async (url, init) => {
2222
2222
  const timeout = 3e4;
2223
2223
  const controller = new AbortController();
2224
2224
  const timeoutId = setTimeout(() => controller.abort(), timeout);
@@ -2226,7 +2226,17 @@ var MCPClient = class {
2226
2226
  // @ts-ignore: AbortSignal.any is available in Node 20+
2227
2227
  AbortSignal.any ? AbortSignal.any([init.signal, controller.signal]) : controller.signal
2228
2228
  ) : controller.signal;
2229
- return fetch(url, { ...init, signal }).finally(() => clearTimeout(timeoutId));
2229
+ try {
2230
+ const response = await fetch(url, { ...init, signal });
2231
+ const hasSessionHeader = init?.headers && new Headers(init.headers).has("mcp-session-id");
2232
+ if (response.status === 404 && hasSessionHeader) {
2233
+ this.client = null;
2234
+ throw new Error("MCP_SESSION_EXPIRED: Downstream session was not found on the server.");
2235
+ }
2236
+ return response;
2237
+ } finally {
2238
+ clearTimeout(timeoutId);
2239
+ }
2230
2240
  }
2231
2241
  };
2232
2242
  if (type === "sse") {
@@ -2418,6 +2428,14 @@ var MCPClient = class {
2418
2428
  * @throws {Error} When connection fails for other reasons
2419
2429
  */
2420
2430
  async connect() {
2431
+ if (this.client?.transport) {
2432
+ this.transport = null;
2433
+ try {
2434
+ await this.client.close();
2435
+ } catch {
2436
+ }
2437
+ this.client = null;
2438
+ }
2421
2439
  await this.initialize();
2422
2440
  if (!this.client || !this.oauthProvider) {
2423
2441
  const error = "Client or OAuth provider not initialized";
@@ -2796,7 +2814,7 @@ var MCPClient = class {
2796
2814
  await this.oauthProvider.invalidateCredentials("all");
2797
2815
  }
2798
2816
  await sessions.delete(this.userId, this.sessionId);
2799
- this.disconnect();
2817
+ await this.disconnect();
2800
2818
  }
2801
2819
  /**
2802
2820
  * Checks if the client is currently connected to an MCP server
@@ -2806,10 +2824,21 @@ var MCPClient = class {
2806
2824
  return this.client !== null;
2807
2825
  }
2808
2826
  /**
2809
- * Disconnects from the MCP server and cleans up resources
2810
- * Does not remove session from Redis - use clearSession() for that
2827
+ * Disconnects from the MCP server and cleans up resources.
2828
+ * Does not remove session from Redis use clearSession() for that.
2829
+ *
2830
+ * For Streamable HTTP sessions, sends an HTTP DELETE to the MCP endpoint
2831
+ * before closing, as recommended by the MCP Streamable HTTP spec
2832
+ * (section "Session Management", rule 5). This is best-effort — errors
2833
+ * (e.g. server already restarted, 404/405 responses) are silently ignored.
2811
2834
  */
2812
- disconnect(reason) {
2835
+ async disconnect() {
2836
+ if (this.transport instanceof StreamableHTTPClientTransport) {
2837
+ try {
2838
+ await this.transport.terminateSession();
2839
+ } catch {
2840
+ }
2841
+ }
2813
2842
  if (this.client) {
2814
2843
  this.client.close();
2815
2844
  }
@@ -2821,7 +2850,6 @@ var MCPClient = class {
2821
2850
  type: "disconnected",
2822
2851
  sessionId: this.sessionId,
2823
2852
  serverId: this.serverId,
2824
- reason,
2825
2853
  timestamp: Date.now()
2826
2854
  });
2827
2855
  this._onObservabilityEvent.fire({
@@ -2830,9 +2858,7 @@ var MCPClient = class {
2830
2858
  message: `Disconnected from ${this.serverId}`,
2831
2859
  sessionId: this.sessionId,
2832
2860
  serverId: this.serverId,
2833
- payload: {
2834
- reason: reason || "unknown"
2835
- },
2861
+ payload: {},
2836
2862
  timestamp: Date.now(),
2837
2863
  id: nanoid()
2838
2864
  });
@@ -2950,6 +2976,9 @@ var MultiSessionClient = class {
2950
2976
  * Connects a single session, with built-in deduplication to prevent race conditions.
2951
2977
  *
2952
2978
  * - If a client for this session already exists and is connected, returns immediately.
2979
+ * - If the existing client entry is no longer connected (e.g. it was explicitly
2980
+ * disconnected), it is evicted so that `establishConnectionWithRetries` creates a
2981
+ * fresh transport — preventing "Client already connected" errors from the SDK.
2953
2982
  * - If a connection attempt for this session is already in-flight (e.g. from a
2954
2983
  * concurrent call), it joins the existing promise instead of starting a new one.
2955
2984
  * This is the key concurrency lock — the `connectionPromises` map acts as a
@@ -2957,9 +2986,12 @@ var MultiSessionClient = class {
2957
2986
  * - On completion (success or failure), the promise is cleaned up from the map.
2958
2987
  */
2959
2988
  async connectSession(session) {
2960
- const existingClient = this.clients.find((c) => c.getSessionId() === session.sessionId);
2961
- if (existingClient?.isConnected()) {
2962
- return;
2989
+ const existing = this.clients.find((c) => c.getSessionId() === session.sessionId);
2990
+ if (existing) {
2991
+ if (existing.isConnected()) {
2992
+ return;
2993
+ }
2994
+ this.clients = this.clients.filter((c) => c !== existing);
2963
2995
  }
2964
2996
  if (this.connectionPromises.has(session.sessionId)) {
2965
2997
  return this.connectionPromises.get(session.sessionId);
@@ -3034,8 +3066,24 @@ var MultiSessionClient = class {
3034
3066
  */
3035
3067
  async connect() {
3036
3068
  const sessions2 = await this.getActiveSessions();
3069
+ const activeSessionIds = new Set(sessions2.map((s) => s.sessionId));
3070
+ this.clients = this.clients.filter((c) => activeSessionIds.has(c.getSessionId()));
3037
3071
  await this.connectInBatches(sessions2);
3038
3072
  }
3073
+ /**
3074
+ * Drops all cached `MCPClient` instances and reconnects fresh from storage.
3075
+ *
3076
+ * Call this when downstream MCP servers have expired their transport sessions
3077
+ * (e.g. after a remote server restart) and subsequent tool calls return
3078
+ * "Session not found. Reconnect without session header." errors.
3079
+ *
3080
+ * OAuth tokens are preserved in the storage backend — no re-authentication
3081
+ * is required. Only the in-memory transport sessions are cleared.
3082
+ */
3083
+ async reconnect() {
3084
+ await this.disconnect();
3085
+ await this.connect();
3086
+ }
3039
3087
  /**
3040
3088
  * Returns all currently connected `MCPClient` instances.
3041
3089
  *
@@ -3048,11 +3096,15 @@ var MultiSessionClient = class {
3048
3096
  /**
3049
3097
  * Gracefully disconnects all active MCP clients and clears the internal client list.
3050
3098
  *
3099
+ * For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
3100
+ * endpoint per the spec before closing locally. All disconnects run in
3101
+ * parallel so shutdown is not serialised across many sessions.
3102
+ *
3051
3103
  * Call this during server shutdown or when a user logs out to free up
3052
3104
  * underlying transport resources (SSE streams, HTTP connections, etc.).
3053
3105
  */
3054
- disconnect() {
3055
- this.clients.forEach((client) => client.disconnect());
3106
+ async disconnect() {
3107
+ await Promise.all(this.clients.map((client) => client.disconnect()));
3056
3108
  this.clients = [];
3057
3109
  }
3058
3110
  };
@@ -3282,7 +3334,6 @@ var SSEConnectionManager = class {
3282
3334
  const client = this.clients.get(sessionId);
3283
3335
  if (client) {
3284
3336
  await client.clearSession();
3285
- client.disconnect();
3286
3337
  this.clients.delete(sessionId);
3287
3338
  } else {
3288
3339
  await sessions.delete(this.userId, sessionId);
@@ -3491,14 +3542,14 @@ var SSEConnectionManager = class {
3491
3542
  /**
3492
3543
  * Cleanup and close all connections
3493
3544
  */
3494
- dispose() {
3545
+ async dispose() {
3495
3546
  this.isActive = false;
3496
3547
  if (this.heartbeatTimer) {
3497
3548
  clearInterval(this.heartbeatTimer);
3498
3549
  }
3499
- for (const client of this.clients.values()) {
3500
- client.disconnect();
3501
- }
3550
+ await Promise.all(
3551
+ Array.from(this.clients.values()).map((client) => client.disconnect())
3552
+ );
3502
3553
  this.clients.clear();
3503
3554
  }
3504
3555
  };