@mcp-ts/sdk 2.4.1 → 2.4.3

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 (49) hide show
  1. package/dist/adapters/agui-adapter.d.mts +3 -4
  2. package/dist/adapters/agui-adapter.d.ts +3 -4
  3. package/dist/adapters/agui-middleware.d.mts +3 -4
  4. package/dist/adapters/agui-middleware.d.ts +3 -4
  5. package/dist/adapters/ai-adapter.d.mts +3 -4
  6. package/dist/adapters/ai-adapter.d.ts +3 -4
  7. package/dist/adapters/langchain-adapter.d.mts +3 -4
  8. package/dist/adapters/langchain-adapter.d.ts +3 -4
  9. package/dist/adapters/mastra-adapter.d.mts +2 -2
  10. package/dist/adapters/mastra-adapter.d.ts +2 -2
  11. package/dist/bin/mcp-ts.js +5 -5
  12. package/dist/bin/mcp-ts.js.map +1 -1
  13. package/dist/bin/mcp-ts.mjs +5 -5
  14. package/dist/bin/mcp-ts.mjs.map +1 -1
  15. package/dist/client/index.d.mts +2 -3
  16. package/dist/client/index.d.ts +2 -3
  17. package/dist/client/react.d.mts +4 -6
  18. package/dist/client/react.d.ts +4 -6
  19. package/dist/client/vue.d.mts +4 -6
  20. package/dist/client/vue.d.ts +4 -6
  21. package/dist/{index-Ch7ouNSa.d.ts → index-B8kJSrBJ.d.ts} +1 -2
  22. package/dist/{index-L5XoXgsb.d.mts → index-DiJsm_lK.d.mts} +1 -2
  23. package/dist/index.d.mts +5 -6
  24. package/dist/index.d.ts +5 -6
  25. package/dist/index.js +174 -110
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +174 -110
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/{multi-session-client-k-9RvWvi.d.mts → multi-session-client-B6hsYO8V.d.mts} +218 -71
  30. package/dist/{multi-session-client-Bwm-efqg.d.ts → multi-session-client-DOBvtaQV.d.ts} +218 -71
  31. package/dist/server/index.d.mts +5 -128
  32. package/dist/server/index.d.ts +5 -128
  33. package/dist/server/index.js +174 -110
  34. package/dist/server/index.js.map +1 -1
  35. package/dist/server/index.mjs +174 -110
  36. package/dist/server/index.mjs.map +1 -1
  37. package/dist/shared/index.d.mts +4 -5
  38. package/dist/shared/index.d.ts +4 -5
  39. package/dist/{tool-router-CuApsDiV.d.mts → tool-router-CbG4Tum6.d.mts} +1 -1
  40. package/dist/{tool-router-CZMrOG8J.d.ts → tool-router-ChIhPwgP.d.ts} +1 -1
  41. package/dist/{types-DCk_IF4L.d.mts → types-CjczQwNX.d.mts} +122 -1
  42. package/dist/{types-DCk_IF4L.d.ts → types-CjczQwNX.d.ts} +122 -1
  43. package/package.json +1 -1
  44. package/src/bin/mcp-ts.ts +5 -5
  45. package/src/server/mcp/multi-session-client.ts +190 -132
  46. package/src/server/mcp/oauth-client.ts +49 -7
  47. package/src/server/storage/index.ts +8 -8
  48. package/dist/events-C4m7tK1U.d.mts +0 -122
  49. package/dist/events-C4m7tK1U.d.ts +0 -122
package/dist/index.js CHANGED
@@ -1741,12 +1741,12 @@ async function createStorage() {
1741
1741
  }
1742
1742
  if (type === "supabase") {
1743
1743
  const url = process.env.SUPABASE_URL;
1744
- const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_ANON_KEY;
1744
+ const key = process.env.SUPABASE_SECRET_KEY || process.env.SUPABASE_ANON_KEY;
1745
1745
  if (!url || !key) {
1746
- console.warn('[mcp-ts][Storage] Explicit selection "supabase" requires SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY.');
1746
+ console.warn('[mcp-ts][Storage] Explicit selection "supabase" requires SUPABASE_URL and SUPABASE_SECRET_KEY.');
1747
1747
  } else {
1748
- if (!process.env.SUPABASE_SERVICE_ROLE_KEY) {
1749
- console.warn('[mcp-ts][Storage] \u26A0\uFE0F Warning: Using "SUPABASE_ANON_KEY" for server-side storage. You may encounter RLS policy violations. "SUPABASE_SERVICE_ROLE_KEY" is recommended.');
1748
+ if (!process.env.SUPABASE_SECRET_KEY) {
1749
+ console.warn('[mcp-ts][Storage] \u26A0\uFE0F Warning: Using "SUPABASE_ANON_KEY" for server-side storage. You may encounter RLS policy violations. "SUPABASE_SECRET_KEY" is recommended.');
1750
1750
  }
1751
1751
  try {
1752
1752
  const { createClient } = await import('@supabase/supabase-js');
@@ -1801,13 +1801,13 @@ async function createStorage() {
1801
1801
  console.log(`[mcp-ts][Storage] Auto-detection: "sqlite" (${process.env.MCP_TS_STORAGE_SQLITE_PATH})`);
1802
1802
  return await initializeStorage(new SqliteStorage({ path: process.env.MCP_TS_STORAGE_SQLITE_PATH }));
1803
1803
  }
1804
- if (process.env.SUPABASE_URL && (process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_ANON_KEY)) {
1804
+ if (process.env.SUPABASE_URL && (process.env.SUPABASE_SECRET_KEY || process.env.SUPABASE_ANON_KEY)) {
1805
1805
  try {
1806
1806
  const { createClient } = await import('@supabase/supabase-js');
1807
1807
  const url = process.env.SUPABASE_URL;
1808
- const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_ANON_KEY;
1809
- if (!process.env.SUPABASE_SERVICE_ROLE_KEY) {
1810
- console.warn('[mcp-ts][Storage] \u26A0\uFE0F Warning: Using "SUPABASE_ANON_KEY" for server-side storage. You may encounter RLS policy violations. "SUPABASE_SERVICE_ROLE_KEY" is recommended.');
1808
+ const key = process.env.SUPABASE_SECRET_KEY || process.env.SUPABASE_ANON_KEY;
1809
+ if (!process.env.SUPABASE_SECRET_KEY) {
1810
+ console.warn('[mcp-ts][Storage] \u26A0\uFE0F Warning: Using "SUPABASE_ANON_KEY" for server-side storage. You may encounter RLS policy violations. "SUPABASE_SECRET_KEY" is recommended.');
1811
1811
  }
1812
1812
  const client = createClient(url, key);
1813
1813
  console.log('[mcp-ts][Storage] Auto-detection: "supabase" (via SUPABASE_URL)');
@@ -2375,7 +2375,6 @@ var MCPClient = class {
2375
2375
  const response = await fetch(url, { ...init, signal });
2376
2376
  const hasSessionHeader = init?.headers && new Headers(init.headers).has("mcp-session-id");
2377
2377
  if (response.status === 404 && hasSessionHeader) {
2378
- this.client = null;
2379
2378
  throw new Error("MCP_SESSION_EXPIRED: Downstream session was not found on the server.");
2380
2379
  }
2381
2380
  return response;
@@ -2756,7 +2755,9 @@ var MCPClient = class {
2756
2755
  method: "tools/list",
2757
2756
  params: {}
2758
2757
  };
2759
- const result = await this.client.request(request, types_js.ListToolsResultSchema);
2758
+ const result = await this.withRetry(
2759
+ () => this.client.request(request, types_js.ListToolsResultSchema)
2760
+ );
2760
2761
  if (this.serverId) {
2761
2762
  this._onConnectionEvent.fire({
2762
2763
  type: "tools_discovered",
@@ -2796,7 +2797,9 @@ var MCPClient = class {
2796
2797
  }
2797
2798
  };
2798
2799
  try {
2799
- const result = await this.client.request(request, types_js.CallToolResultSchema);
2800
+ const result = await this.withRetry(
2801
+ () => this.client.request(request, types_js.CallToolResultSchema)
2802
+ );
2800
2803
  this._onObservabilityEvent.fire({
2801
2804
  type: "mcp:client:tool_call",
2802
2805
  level: "info",
@@ -2848,7 +2851,9 @@ var MCPClient = class {
2848
2851
  method: "prompts/list",
2849
2852
  params: {}
2850
2853
  };
2851
- const result = await this.client.request(request, types_js.ListPromptsResultSchema);
2854
+ const result = await this.withRetry(
2855
+ () => this.client.request(request, types_js.ListPromptsResultSchema)
2856
+ );
2852
2857
  this.emitStateChange("READY");
2853
2858
  this.emitProgress(`Discovered ${result.prompts.length} prompts`);
2854
2859
  return result;
@@ -2877,7 +2882,9 @@ var MCPClient = class {
2877
2882
  arguments: args
2878
2883
  }
2879
2884
  };
2880
- return await this.client.request(request, types_js.GetPromptResultSchema);
2885
+ return await this.withRetry(
2886
+ () => this.client.request(request, types_js.GetPromptResultSchema)
2887
+ );
2881
2888
  }
2882
2889
  /**
2883
2890
  * Lists all available resources from the connected MCP server
@@ -2894,7 +2901,9 @@ var MCPClient = class {
2894
2901
  method: "resources/list",
2895
2902
  params: {}
2896
2903
  };
2897
- const result = await this.client.request(request, types_js.ListResourcesResultSchema);
2904
+ const result = await this.withRetry(
2905
+ () => this.client.request(request, types_js.ListResourcesResultSchema)
2906
+ );
2898
2907
  this.emitStateChange("READY");
2899
2908
  this.emitProgress(`Discovered ${result.resources.length} resources`);
2900
2909
  return result;
@@ -2921,7 +2930,36 @@ var MCPClient = class {
2921
2930
  uri
2922
2931
  }
2923
2932
  };
2924
- return await this.client.request(request, types_js.ReadResourceResultSchema);
2933
+ return await this.withRetry(
2934
+ () => this.client.request(request, types_js.ReadResourceResultSchema)
2935
+ );
2936
+ }
2937
+ /**
2938
+ * Wraps an MCP request with automatic transport-session recovery.
2939
+ *
2940
+ * When the downstream MCP server rejects the request with a 404 indicating
2941
+ * the transport session has expired, this method tears down the stale SDK
2942
+ * client and transport, calls {@link reconnect} to negotiate a fresh session,
2943
+ * and retries the request once.
2944
+ *
2945
+ * Non-transient errors (network failures, auth errors, etc.) propagate as-is.
2946
+ */
2947
+ async withRetry(fn) {
2948
+ try {
2949
+ return await fn();
2950
+ } catch (error) {
2951
+ if (!(error instanceof Error && error.message.includes("MCP_SESSION_EXPIRED"))) throw error;
2952
+ if (this.client) {
2953
+ try {
2954
+ await this.client.close();
2955
+ } catch {
2956
+ }
2957
+ this.transport = null;
2958
+ this.client = null;
2959
+ }
2960
+ await this.reconnect();
2961
+ return await fn();
2962
+ }
2925
2963
  }
2926
2964
  /**
2927
2965
  * Reconnects to MCP server using existing OAuth provider from Redis
@@ -2934,6 +2972,12 @@ var MCPClient = class {
2934
2972
  if (!this.oauthProvider) {
2935
2973
  throw new Error("OAuth provider not initialized");
2936
2974
  }
2975
+ if (this.client) {
2976
+ try {
2977
+ await this.client.close();
2978
+ } catch {
2979
+ }
2980
+ }
2937
2981
  this.client = new index_js.Client(
2938
2982
  {
2939
2983
  name: MCP_CLIENT_NAME,
@@ -3070,17 +3114,14 @@ var DEFAULT_RETRY_DELAY_MS = 1e3;
3070
3114
  var CONNECTION_BATCH_SIZE = 5;
3071
3115
  var MultiSessionClient = class {
3072
3116
  /**
3073
- * Creates a new MultiSessionClient for the given user userId.
3074
- *
3075
- * @param userId - A unique string identifying the user (e.g. user ID or email).
3076
- * @param options - Optional tuning for connection timeout, retry count, and retry delay.
3077
- * Falls back to sensible defaults if not provided.
3117
+ * @param userId - Unique identifier for the user (e.g. user ID or email).
3118
+ * @param options - Optional tuning and lifecycle hooks.
3078
3119
  */
3079
3120
  constructor(userId, options = {}) {
3080
3121
  __publicField(this, "clients", []);
3122
+ __publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
3081
3123
  __publicField(this, "userId");
3082
3124
  __publicField(this, "options");
3083
- __publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
3084
3125
  this.userId = userId;
3085
3126
  this.options = {
3086
3127
  timeout: DEFAULT_TIMEOUT_MS,
@@ -3089,28 +3130,97 @@ var MultiSessionClient = class {
3089
3130
  ...options
3090
3131
  };
3091
3132
  }
3133
+ // -----------------------------------------------------------------------
3134
+ // Public API
3135
+ // -----------------------------------------------------------------------
3092
3136
  /**
3093
- * Fetches all sessions for this userId from storage and returns only the
3094
- * ones that are ready to connect.
3137
+ * Fetches active sessions and establishes connections to all of them.
3095
3138
  *
3096
- * A session is considered connectable when:
3097
- * - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
3098
- * - Its status is `active`. Pending sessions are skipped here
3099
- * and let the OAuth flow complete separately before we try to reconnect them.
3139
+ * Call this once after creating the client. On long-running servers you
3140
+ * can cache the `MultiSessionClient` instance and call `connect()` on
3141
+ * each request already-connected sessions are skipped internally.
3100
3142
  */
3101
- async getActiveSessions() {
3102
- const sessionList = await sessions.list(this.userId);
3103
- const valid = sessionList.filter(
3143
+ async connect() {
3144
+ const sessions2 = await this.fetchActiveSessions();
3145
+ const activeSessionIds = new Set(sessions2.map((s) => s.sessionId));
3146
+ for (const client of this.clients) {
3147
+ if (!activeSessionIds.has(client.getSessionId())) {
3148
+ this.options.onSessionEvicted?.(client.getSessionId());
3149
+ }
3150
+ }
3151
+ this.clients = this.clients.filter((c) => activeSessionIds.has(c.getSessionId()));
3152
+ await this.connectInBatches(sessions2);
3153
+ }
3154
+ /**
3155
+ * Drops all cached `MCPClient` instances and reconnects fresh from storage.
3156
+ *
3157
+ * Call this when downstream MCP servers have expired their transport sessions
3158
+ * (e.g. after a remote server restart) and subsequent tool calls return
3159
+ * "Session not found. Reconnect without session header." errors.
3160
+ *
3161
+ * OAuth tokens are preserved in the storage backend — no re-authentication
3162
+ * is required. Only the in-memory transport sessions are cleared.
3163
+ */
3164
+ async reconnect() {
3165
+ await this.disconnect();
3166
+ await this.connect();
3167
+ }
3168
+ /**
3169
+ * Returns all currently connected `MCPClient` instances.
3170
+ *
3171
+ * Use this to enumerate available tools across all connected servers,
3172
+ * or to route a tool call to the right client by `serverId`.
3173
+ */
3174
+ getClients() {
3175
+ return this.clients;
3176
+ }
3177
+ /**
3178
+ * Removes and disconnects a single session by ID.
3179
+ *
3180
+ * @returns `true` if the session was found and removed, `false` if not found.
3181
+ */
3182
+ async removeSession(sessionId) {
3183
+ const idx = this.clients.findIndex((c) => c.getSessionId() === sessionId);
3184
+ if (idx === -1) return false;
3185
+ const [client] = this.clients.splice(idx, 1);
3186
+ await client.disconnect();
3187
+ return true;
3188
+ }
3189
+ /**
3190
+ * Gracefully disconnects all active MCP clients and clears the internal list.
3191
+ *
3192
+ * For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
3193
+ * endpoint per the spec before closing locally. All disconnects run in
3194
+ * parallel so shutdown is not serialised across many sessions.
3195
+ *
3196
+ * Call this during server shutdown or when a user logs out to free up
3197
+ * underlying transport resources (SSE streams, HTTP connections, etc.).
3198
+ */
3199
+ async disconnect() {
3200
+ await Promise.all(this.clients.map((client) => client.disconnect()));
3201
+ this.clients = [];
3202
+ }
3203
+ // -----------------------------------------------------------------------
3204
+ // Internals
3205
+ // -----------------------------------------------------------------------
3206
+ /**
3207
+ * Resolves the list of sessions to connect.
3208
+ *
3209
+ * Uses the custom `sessionProvider` when provided, otherwise falls back
3210
+ * to querying the storage backend via `sessions.list(userId)`.
3211
+ */
3212
+ async fetchActiveSessions() {
3213
+ const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await sessions.list(this.userId);
3214
+ return sessionList.filter(
3104
3215
  (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
3105
3216
  );
3106
- return valid;
3107
3217
  }
3108
3218
  /**
3109
- * Connects to a list of sessions in controlled batches of `CONNECTION_BATCH_SIZE`.
3219
+ * Connects a list of sessions in controlled batches.
3110
3220
  *
3111
- * Batching prevents overwhelming the event loop or external servers when a user
3112
- * has many active MCP sessions (e.g. 20+ servers). Within each batch, sessions
3113
- * are connected concurrently using `Promise.all` for speed.
3221
+ * Batching prevents overwhelming the event loop or external servers when
3222
+ * a user has many active MCP sessions. Within each batch, sessions are
3223
+ * connected concurrently using `Promise.all`.
3114
3224
  */
3115
3225
  async connectInBatches(sessions2) {
3116
3226
  for (let i = 0; i < sessions2.length; i += CONNECTION_BATCH_SIZE) {
@@ -3119,17 +3229,16 @@ var MultiSessionClient = class {
3119
3229
  }
3120
3230
  }
3121
3231
  /**
3122
- * Connects a single session, with built-in deduplication to prevent race conditions.
3232
+ * Connects a single session, with deduplication to prevent race conditions.
3123
3233
  *
3124
- * - If a client for this session already exists and is connected, returns immediately.
3125
- * - If the existing client entry is no longer connected (e.g. it was explicitly
3126
- * disconnected), it is evicted so that `establishConnectionWithRetries` creates a
3127
- * fresh transport preventing "Client already connected" errors from the SDK.
3128
- * - If a connection attempt for this session is already in-flight (e.g. from a
3129
- * concurrent call), it joins the existing promise instead of starting a new one.
3130
- * This is the key concurrency lock the `connectionPromises` map acts as a
3131
- * per-session mutex so we never spin up two physical connections for the same session.
3132
- * - On completion (success or failure), the promise is cleaned up from the map.
3234
+ * - If a client for this session already exists and is connected, returns
3235
+ * immediately.
3236
+ * - If the existing client entry is no longer connected (e.g. explicit
3237
+ * disconnect), it is evicted so a fresh transport is created.
3238
+ * - If a connection attempt for this session is already in-flight, the
3239
+ * existing promise is reused as a per-session mutex.
3240
+ * - On completion (success or failure), the promise is cleaned up from
3241
+ * the connectionPromises map.
3133
3242
  */
3134
3243
  async connectSession(session) {
3135
3244
  const existing = this.clients.find((c) => c.getSessionId() === session.sessionId);
@@ -3137,6 +3246,7 @@ var MultiSessionClient = class {
3137
3246
  if (existing.isConnected()) {
3138
3247
  return;
3139
3248
  }
3249
+ this.options.onSessionEvicted?.(existing.getSessionId());
3140
3250
  this.clients = this.clients.filter((c) => c !== existing);
3141
3251
  }
3142
3252
  if (this.connectionPromises.has(session.sessionId)) {
@@ -3151,22 +3261,19 @@ var MultiSessionClient = class {
3151
3261
  }
3152
3262
  }
3153
3263
  /**
3154
- * The core connection loop for a single session.
3264
+ * Core connection loop for a single session with retry logic.
3155
3265
  *
3156
- * Attempts to establish a physical MCP connection, retrying up to `maxRetries` times
3157
- * if the connection fails. Each attempt:
3158
- * 1. Creates a fresh `MCPClient` instance from the session data.
3159
- * 2. Races the connect call against a timeout promise — if the server doesn't respond
3160
- * within `timeoutMs`, the attempt is aborted and counted as a failure.
3161
- * 3. On success, replaces any stale client entry for this session in the `clients` array.
3266
+ * 1. Creates a fresh `MCPClient` from the session data.
3267
+ * 2. Races `client.connect()` against a timeout.
3268
+ * 3. On success, replaces any stale entry and fires `onSessionConnected`.
3162
3269
  * 4. On failure, waits `retryDelay` ms before the next attempt.
3163
3270
  *
3164
- * If all attempts are exhausted, logs an error and returns silently (does not throw),
3165
- * so a single bad server doesn't block the rest of the batch from connecting.
3271
+ * If all attempts are exhausted, logs an error and returns silently so
3272
+ * a single bad server doesn't block the rest of the batch.
3166
3273
  */
3167
3274
  async establishConnectionWithRetries(session) {
3168
- const maxRetries = this.options.maxRetries ?? DEFAULT_MAX_RETRIES;
3169
- const retryDelay = this.options.retryDelay ?? DEFAULT_RETRY_DELAY_MS;
3275
+ const maxRetries = this.options.maxRetries;
3276
+ const retryDelay = this.options.retryDelay;
3170
3277
  let lastError;
3171
3278
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
3172
3279
  try {
@@ -3180,10 +3287,13 @@ var MultiSessionClient = class {
3180
3287
  transportType: session.transportType,
3181
3288
  headers: session.headers
3182
3289
  });
3183
- const timeoutMs = this.options.timeout ?? DEFAULT_TIMEOUT_MS;
3290
+ const timeoutMs = this.options.timeout;
3184
3291
  let timeoutTimer;
3185
3292
  const timeoutPromise = new Promise((_, reject) => {
3186
- timeoutTimer = setTimeout(() => reject(new Error(`Connection timed out after ${timeoutMs}ms`)), timeoutMs);
3293
+ timeoutTimer = setTimeout(
3294
+ () => reject(new Error(`Connection timed out after ${timeoutMs}ms`)),
3295
+ timeoutMs
3296
+ );
3187
3297
  });
3188
3298
  try {
3189
3299
  await Promise.race([client.connect(), timeoutPromise]);
@@ -3192,6 +3302,7 @@ var MultiSessionClient = class {
3192
3302
  }
3193
3303
  this.clients = this.clients.filter((c) => c.getSessionId() !== session.sessionId);
3194
3304
  this.clients.push(client);
3305
+ this.options.onSessionConnected?.(session.sessionId, client);
3195
3306
  return;
3196
3307
  } catch (error) {
3197
3308
  lastError = error;
@@ -3200,58 +3311,11 @@ var MultiSessionClient = class {
3200
3311
  }
3201
3312
  }
3202
3313
  }
3203
- console.error(`[MultiSessionClient] Failed to connect to session ${session.sessionId} after ${maxRetries + 1} attempts:`, lastError);
3204
- }
3205
- /**
3206
- * The main entry point. Fetches all active sessions for this userId from
3207
- * storage and establishes connections to all of them in batches.
3208
- *
3209
- * Call this once after creating the client. On traditional servers, you can
3210
- * cache the `MultiSessionClient` instance after calling `connect()` to avoid
3211
- * re-fetching and re-connecting on every request.
3212
- */
3213
- async connect() {
3214
- const sessions2 = await this.getActiveSessions();
3215
- const activeSessionIds = new Set(sessions2.map((s) => s.sessionId));
3216
- this.clients = this.clients.filter((c) => activeSessionIds.has(c.getSessionId()));
3217
- await this.connectInBatches(sessions2);
3218
- }
3219
- /**
3220
- * Drops all cached `MCPClient` instances and reconnects fresh from storage.
3221
- *
3222
- * Call this when downstream MCP servers have expired their transport sessions
3223
- * (e.g. after a remote server restart) and subsequent tool calls return
3224
- * "Session not found. Reconnect without session header." errors.
3225
- *
3226
- * OAuth tokens are preserved in the storage backend — no re-authentication
3227
- * is required. Only the in-memory transport sessions are cleared.
3228
- */
3229
- async reconnect() {
3230
- await this.disconnect();
3231
- await this.connect();
3232
- }
3233
- /**
3234
- * Returns all currently connected `MCPClient` instances.
3235
- *
3236
- * Use this to enumerate available tools across all connected servers,
3237
- * or to route a tool call to the right client by `serverId`.
3238
- */
3239
- getClients() {
3240
- return this.clients;
3241
- }
3242
- /**
3243
- * Gracefully disconnects all active MCP clients and clears the internal client list.
3244
- *
3245
- * For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
3246
- * endpoint per the spec before closing locally. All disconnects run in
3247
- * parallel so shutdown is not serialised across many sessions.
3248
- *
3249
- * Call this during server shutdown or when a user logs out to free up
3250
- * underlying transport resources (SSE streams, HTTP connections, etc.).
3251
- */
3252
- async disconnect() {
3253
- await Promise.all(this.clients.map((client) => client.disconnect()));
3254
- this.clients = [];
3314
+ this.options.onSessionFailed?.(session.sessionId, lastError);
3315
+ console.error(
3316
+ `[MultiSessionClient] Failed to connect to session ${session.sessionId} after ${maxRetries + 1} attempts:`,
3317
+ lastError
3318
+ );
3255
3319
  }
3256
3320
  };
3257
3321