@mcp-ts/sdk 2.4.0 → 2.4.2

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 (52) hide show
  1. package/README.md +58 -19
  2. package/dist/adapters/agui-adapter.d.mts +3 -4
  3. package/dist/adapters/agui-adapter.d.ts +3 -4
  4. package/dist/adapters/agui-middleware.d.mts +3 -4
  5. package/dist/adapters/agui-middleware.d.ts +3 -4
  6. package/dist/adapters/ai-adapter.d.mts +3 -4
  7. package/dist/adapters/ai-adapter.d.ts +3 -4
  8. package/dist/adapters/langchain-adapter.d.mts +3 -4
  9. package/dist/adapters/langchain-adapter.d.ts +3 -4
  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 -3
  13. package/dist/client/index.d.ts +2 -3
  14. package/dist/client/react.d.mts +4 -6
  15. package/dist/client/react.d.ts +4 -6
  16. package/dist/client/vue.d.mts +4 -6
  17. package/dist/client/vue.d.ts +4 -6
  18. package/dist/{index-CtXvKl8N.d.ts → index-B8kJSrBJ.d.ts} +1 -2
  19. package/dist/{index-ByIjEReo.d.mts → index-DiJsm_lK.d.mts} +1 -2
  20. package/dist/index.d.mts +5 -6
  21. package/dist/index.d.ts +5 -6
  22. package/dist/index.js +149 -90
  23. package/dist/index.js.map +1 -1
  24. package/dist/index.mjs +149 -90
  25. package/dist/index.mjs.map +1 -1
  26. package/dist/{multi-session-client-CnvZEGPY.d.ts → multi-session-client-BluyCPo9.d.ts} +219 -66
  27. package/dist/{multi-session-client-CIMUGF8S.d.mts → multi-session-client-CWs-AE78.d.mts} +219 -66
  28. package/dist/server/index.d.mts +6 -129
  29. package/dist/server/index.d.ts +6 -129
  30. package/dist/server/index.js +149 -90
  31. package/dist/server/index.js.map +1 -1
  32. package/dist/server/index.mjs +149 -90
  33. package/dist/server/index.mjs.map +1 -1
  34. package/dist/shared/index.d.mts +4 -5
  35. package/dist/shared/index.d.ts +4 -5
  36. package/dist/shared/index.js.map +1 -1
  37. package/dist/shared/index.mjs.map +1 -1
  38. package/dist/{tool-router-CuApsDiV.d.mts → tool-router-CbG4Tum6.d.mts} +1 -1
  39. package/dist/{tool-router-CZMrOG8J.d.ts → tool-router-ChIhPwgP.d.ts} +1 -1
  40. package/dist/{types-DCk_IF4L.d.ts → types-CjczQwNX.d.mts} +122 -1
  41. package/dist/{types-DCk_IF4L.d.mts → types-CjczQwNX.d.ts} +122 -1
  42. package/migrations/v2.3.4/neon/20260513010000_install_mcp_sessions.sql +69 -0
  43. package/migrations/v2.3.4/neon/20260513020000_add_session_cleanup_cron.sql +35 -0
  44. package/migrations/v2.3.4/supabase/20260330195700_install_mcp_sessions.sql +82 -0
  45. package/migrations/v2.3.4/supabase/20260421010000_add_session_cleanup_cron.sql +32 -0
  46. package/package.json +1 -1
  47. package/src/server/handlers/sse-handler.ts +7 -5
  48. package/src/server/mcp/multi-session-client.ts +186 -100
  49. package/src/server/mcp/oauth-client.ts +60 -10
  50. package/src/shared/events.ts +0 -1
  51. package/dist/events-CK3N--3g.d.mts +0 -123
  52. package/dist/events-CK3N--3g.d.ts +0 -123
@@ -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
  });
@@ -2898,17 +2924,14 @@ var DEFAULT_RETRY_DELAY_MS = 1e3;
2898
2924
  var CONNECTION_BATCH_SIZE = 5;
2899
2925
  var MultiSessionClient = class {
2900
2926
  /**
2901
- * Creates a new MultiSessionClient for the given user userId.
2902
- *
2903
- * @param userId - A unique string identifying the user (e.g. user ID or email).
2904
- * @param options - Optional tuning for connection timeout, retry count, and retry delay.
2905
- * Falls back to sensible defaults if not provided.
2927
+ * @param userId - Unique identifier for the user (e.g. user ID or email).
2928
+ * @param options - Optional tuning and lifecycle hooks.
2906
2929
  */
2907
2930
  constructor(userId, options = {}) {
2908
2931
  __publicField(this, "clients", []);
2932
+ __publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
2909
2933
  __publicField(this, "userId");
2910
2934
  __publicField(this, "options");
2911
- __publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
2912
2935
  this.userId = userId;
2913
2936
  this.options = {
2914
2937
  timeout: DEFAULT_TIMEOUT_MS,
@@ -2917,28 +2940,85 @@ var MultiSessionClient = class {
2917
2940
  ...options
2918
2941
  };
2919
2942
  }
2943
+ // -----------------------------------------------------------------------
2944
+ // Public API
2945
+ // -----------------------------------------------------------------------
2920
2946
  /**
2921
- * Fetches all sessions for this userId from storage and returns only the
2922
- * ones that are ready to connect.
2947
+ * Fetches active sessions and establishes connections to all of them.
2923
2948
  *
2924
- * A session is considered connectable when:
2925
- * - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
2926
- * - Its status is `active`. Pending sessions are skipped here
2927
- * and let the OAuth flow complete separately before we try to reconnect them.
2949
+ * Call this once after creating the client. On long-running servers you
2950
+ * can cache the `MultiSessionClient` instance and call `connect()` on
2951
+ * each request already-connected sessions are skipped internally.
2928
2952
  */
2929
- async getActiveSessions() {
2930
- const sessionList = await sessions.list(this.userId);
2931
- const valid = sessionList.filter(
2953
+ async connect() {
2954
+ const sessions2 = await this.fetchActiveSessions();
2955
+ const activeSessionIds = new Set(sessions2.map((s) => s.sessionId));
2956
+ for (const client of this.clients) {
2957
+ if (!activeSessionIds.has(client.getSessionId())) {
2958
+ this.options.onSessionEvicted?.(client.getSessionId());
2959
+ }
2960
+ }
2961
+ this.clients = this.clients.filter((c) => activeSessionIds.has(c.getSessionId()));
2962
+ await this.connectInBatches(sessions2);
2963
+ }
2964
+ /**
2965
+ * Drops all cached `MCPClient` instances and reconnects fresh from storage.
2966
+ *
2967
+ * Call this when downstream MCP servers have expired their transport sessions
2968
+ * (e.g. after a remote server restart) and subsequent tool calls return
2969
+ * "Session not found. Reconnect without session header." errors.
2970
+ *
2971
+ * OAuth tokens are preserved in the storage backend — no re-authentication
2972
+ * is required. Only the in-memory transport sessions are cleared.
2973
+ */
2974
+ async reconnect() {
2975
+ await this.disconnect();
2976
+ await this.connect();
2977
+ }
2978
+ /**
2979
+ * Returns all currently connected `MCPClient` instances.
2980
+ *
2981
+ * Use this to enumerate available tools across all connected servers,
2982
+ * or to route a tool call to the right client by `serverId`.
2983
+ */
2984
+ getClients() {
2985
+ return this.clients;
2986
+ }
2987
+ /**
2988
+ * Gracefully disconnects all active MCP clients and clears the internal list.
2989
+ *
2990
+ * For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
2991
+ * endpoint per the spec before closing locally. All disconnects run in
2992
+ * parallel so shutdown is not serialised across many sessions.
2993
+ *
2994
+ * Call this during server shutdown or when a user logs out to free up
2995
+ * underlying transport resources (SSE streams, HTTP connections, etc.).
2996
+ */
2997
+ async disconnect() {
2998
+ await Promise.all(this.clients.map((client) => client.disconnect()));
2999
+ this.clients = [];
3000
+ }
3001
+ // -----------------------------------------------------------------------
3002
+ // Internals
3003
+ // -----------------------------------------------------------------------
3004
+ /**
3005
+ * Resolves the list of sessions to connect.
3006
+ *
3007
+ * Uses the custom `sessionProvider` when provided, otherwise falls back
3008
+ * to querying the storage backend via `sessions.list(userId)`.
3009
+ */
3010
+ async fetchActiveSessions() {
3011
+ const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await sessions.list(this.userId);
3012
+ return sessionList.filter(
2932
3013
  (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
2933
3014
  );
2934
- return valid;
2935
3015
  }
2936
3016
  /**
2937
- * Connects to a list of sessions in controlled batches of `CONNECTION_BATCH_SIZE`.
3017
+ * Connects a list of sessions in controlled batches.
2938
3018
  *
2939
- * Batching prevents overwhelming the event loop or external servers when a user
2940
- * has many active MCP sessions (e.g. 20+ servers). Within each batch, sessions
2941
- * are connected concurrently using `Promise.all` for speed.
3019
+ * Batching prevents overwhelming the event loop or external servers when
3020
+ * a user has many active MCP sessions. Within each batch, sessions are
3021
+ * connected concurrently using `Promise.all`.
2942
3022
  */
2943
3023
  async connectInBatches(sessions2) {
2944
3024
  for (let i = 0; i < sessions2.length; i += CONNECTION_BATCH_SIZE) {
@@ -2947,19 +3027,25 @@ var MultiSessionClient = class {
2947
3027
  }
2948
3028
  }
2949
3029
  /**
2950
- * Connects a single session, with built-in deduplication to prevent race conditions.
3030
+ * Connects a single session, with deduplication to prevent race conditions.
2951
3031
  *
2952
- * - If a client for this session already exists and is connected, returns immediately.
2953
- * - If a connection attempt for this session is already in-flight (e.g. from a
2954
- * concurrent call), it joins the existing promise instead of starting a new one.
2955
- * This is the key concurrency lock the `connectionPromises` map acts as a
2956
- * per-session mutex so we never spin up two physical connections for the same session.
2957
- * - On completion (success or failure), the promise is cleaned up from the map.
3032
+ * - If a client for this session already exists and is connected, returns
3033
+ * immediately.
3034
+ * - If the existing client entry is no longer connected (e.g. explicit
3035
+ * disconnect), it is evicted so a fresh transport is created.
3036
+ * - If a connection attempt for this session is already in-flight, the
3037
+ * existing promise is reused as a per-session mutex.
3038
+ * - On completion (success or failure), the promise is cleaned up from
3039
+ * the connectionPromises map.
2958
3040
  */
2959
3041
  async connectSession(session) {
2960
- const existingClient = this.clients.find((c) => c.getSessionId() === session.sessionId);
2961
- if (existingClient?.isConnected()) {
2962
- return;
3042
+ const existing = this.clients.find((c) => c.getSessionId() === session.sessionId);
3043
+ if (existing) {
3044
+ if (existing.isConnected()) {
3045
+ return;
3046
+ }
3047
+ this.options.onSessionEvicted?.(existing.getSessionId());
3048
+ this.clients = this.clients.filter((c) => c !== existing);
2963
3049
  }
2964
3050
  if (this.connectionPromises.has(session.sessionId)) {
2965
3051
  return this.connectionPromises.get(session.sessionId);
@@ -2973,22 +3059,19 @@ var MultiSessionClient = class {
2973
3059
  }
2974
3060
  }
2975
3061
  /**
2976
- * The core connection loop for a single session.
3062
+ * Core connection loop for a single session with retry logic.
2977
3063
  *
2978
- * Attempts to establish a physical MCP connection, retrying up to `maxRetries` times
2979
- * if the connection fails. Each attempt:
2980
- * 1. Creates a fresh `MCPClient` instance from the session data.
2981
- * 2. Races the connect call against a timeout promise — if the server doesn't respond
2982
- * within `timeoutMs`, the attempt is aborted and counted as a failure.
2983
- * 3. On success, replaces any stale client entry for this session in the `clients` array.
3064
+ * 1. Creates a fresh `MCPClient` from the session data.
3065
+ * 2. Races `client.connect()` against a timeout.
3066
+ * 3. On success, replaces any stale entry and fires `onSessionConnected`.
2984
3067
  * 4. On failure, waits `retryDelay` ms before the next attempt.
2985
3068
  *
2986
- * If all attempts are exhausted, logs an error and returns silently (does not throw),
2987
- * so a single bad server doesn't block the rest of the batch from connecting.
3069
+ * If all attempts are exhausted, logs an error and returns silently so
3070
+ * a single bad server doesn't block the rest of the batch.
2988
3071
  */
2989
3072
  async establishConnectionWithRetries(session) {
2990
- const maxRetries = this.options.maxRetries ?? DEFAULT_MAX_RETRIES;
2991
- const retryDelay = this.options.retryDelay ?? DEFAULT_RETRY_DELAY_MS;
3073
+ const maxRetries = this.options.maxRetries;
3074
+ const retryDelay = this.options.retryDelay;
2992
3075
  let lastError;
2993
3076
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
2994
3077
  try {
@@ -3002,10 +3085,13 @@ var MultiSessionClient = class {
3002
3085
  transportType: session.transportType,
3003
3086
  headers: session.headers
3004
3087
  });
3005
- const timeoutMs = this.options.timeout ?? DEFAULT_TIMEOUT_MS;
3088
+ const timeoutMs = this.options.timeout;
3006
3089
  let timeoutTimer;
3007
3090
  const timeoutPromise = new Promise((_, reject) => {
3008
- timeoutTimer = setTimeout(() => reject(new Error(`Connection timed out after ${timeoutMs}ms`)), timeoutMs);
3091
+ timeoutTimer = setTimeout(
3092
+ () => reject(new Error(`Connection timed out after ${timeoutMs}ms`)),
3093
+ timeoutMs
3094
+ );
3009
3095
  });
3010
3096
  try {
3011
3097
  await Promise.race([client.connect(), timeoutPromise]);
@@ -3014,6 +3100,7 @@ var MultiSessionClient = class {
3014
3100
  }
3015
3101
  this.clients = this.clients.filter((c) => c.getSessionId() !== session.sessionId);
3016
3102
  this.clients.push(client);
3103
+ this.options.onSessionConnected?.(session.sessionId, client);
3017
3104
  return;
3018
3105
  } catch (error) {
3019
3106
  lastError = error;
@@ -3022,38 +3109,11 @@ var MultiSessionClient = class {
3022
3109
  }
3023
3110
  }
3024
3111
  }
3025
- console.error(`[MultiSessionClient] Failed to connect to session ${session.sessionId} after ${maxRetries + 1} attempts:`, lastError);
3026
- }
3027
- /**
3028
- * The main entry point. Fetches all active sessions for this userId from
3029
- * storage and establishes connections to all of them in batches.
3030
- *
3031
- * Call this once after creating the client. On traditional servers, you can
3032
- * cache the `MultiSessionClient` instance after calling `connect()` to avoid
3033
- * re-fetching and re-connecting on every request.
3034
- */
3035
- async connect() {
3036
- const sessions2 = await this.getActiveSessions();
3037
- await this.connectInBatches(sessions2);
3038
- }
3039
- /**
3040
- * Returns all currently connected `MCPClient` instances.
3041
- *
3042
- * Use this to enumerate available tools across all connected servers,
3043
- * or to route a tool call to the right client by `serverId`.
3044
- */
3045
- getClients() {
3046
- return this.clients;
3047
- }
3048
- /**
3049
- * Gracefully disconnects all active MCP clients and clears the internal client list.
3050
- *
3051
- * Call this during server shutdown or when a user logs out to free up
3052
- * underlying transport resources (SSE streams, HTTP connections, etc.).
3053
- */
3054
- disconnect() {
3055
- this.clients.forEach((client) => client.disconnect());
3056
- this.clients = [];
3112
+ this.options.onSessionFailed?.(session.sessionId, lastError);
3113
+ console.error(
3114
+ `[MultiSessionClient] Failed to connect to session ${session.sessionId} after ${maxRetries + 1} attempts:`,
3115
+ lastError
3116
+ );
3057
3117
  }
3058
3118
  };
3059
3119
 
@@ -3282,7 +3342,6 @@ var SSEConnectionManager = class {
3282
3342
  const client = this.clients.get(sessionId);
3283
3343
  if (client) {
3284
3344
  await client.clearSession();
3285
- client.disconnect();
3286
3345
  this.clients.delete(sessionId);
3287
3346
  } else {
3288
3347
  await sessions.delete(this.userId, sessionId);
@@ -3491,14 +3550,14 @@ var SSEConnectionManager = class {
3491
3550
  /**
3492
3551
  * Cleanup and close all connections
3493
3552
  */
3494
- dispose() {
3553
+ async dispose() {
3495
3554
  this.isActive = false;
3496
3555
  if (this.heartbeatTimer) {
3497
3556
  clearInterval(this.heartbeatTimer);
3498
3557
  }
3499
- for (const client of this.clients.values()) {
3500
- client.disconnect();
3501
- }
3558
+ await Promise.all(
3559
+ Array.from(this.clients.values()).map((client) => client.disconnect())
3560
+ );
3502
3561
  this.clients.clear();
3503
3562
  }
3504
3563
  };