@mcp-ts/sdk 1.3.6 → 1.3.9

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 (103) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +398 -404
  3. package/dist/adapters/agui-adapter.d.mts +1 -1
  4. package/dist/adapters/agui-adapter.d.ts +1 -1
  5. package/dist/adapters/agui-adapter.js +2 -2
  6. package/dist/adapters/agui-adapter.js.map +1 -1
  7. package/dist/adapters/agui-adapter.mjs +2 -2
  8. package/dist/adapters/agui-adapter.mjs.map +1 -1
  9. package/dist/adapters/agui-middleware.d.mts +1 -1
  10. package/dist/adapters/agui-middleware.d.ts +1 -1
  11. package/dist/adapters/agui-middleware.js.map +1 -1
  12. package/dist/adapters/agui-middleware.mjs.map +1 -1
  13. package/dist/adapters/ai-adapter.d.mts +1 -1
  14. package/dist/adapters/ai-adapter.d.ts +1 -1
  15. package/dist/adapters/ai-adapter.js +1 -1
  16. package/dist/adapters/ai-adapter.js.map +1 -1
  17. package/dist/adapters/ai-adapter.mjs +1 -1
  18. package/dist/adapters/ai-adapter.mjs.map +1 -1
  19. package/dist/adapters/langchain-adapter.d.mts +1 -1
  20. package/dist/adapters/langchain-adapter.d.ts +1 -1
  21. package/dist/adapters/langchain-adapter.js +1 -1
  22. package/dist/adapters/langchain-adapter.js.map +1 -1
  23. package/dist/adapters/langchain-adapter.mjs +1 -1
  24. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  25. package/dist/adapters/mastra-adapter.d.mts +1 -1
  26. package/dist/adapters/mastra-adapter.d.ts +1 -1
  27. package/dist/adapters/mastra-adapter.js +1 -1
  28. package/dist/adapters/mastra-adapter.js.map +1 -1
  29. package/dist/adapters/mastra-adapter.mjs +1 -1
  30. package/dist/adapters/mastra-adapter.mjs.map +1 -1
  31. package/dist/bin/mcp-ts.js +0 -0
  32. package/dist/bin/mcp-ts.js.map +1 -1
  33. package/dist/bin/mcp-ts.mjs +0 -0
  34. package/dist/bin/mcp-ts.mjs.map +1 -1
  35. package/dist/client/index.js.map +1 -1
  36. package/dist/client/index.mjs.map +1 -1
  37. package/dist/client/react.d.mts +2 -2
  38. package/dist/client/react.d.ts +2 -2
  39. package/dist/client/react.js +25 -2
  40. package/dist/client/react.js.map +1 -1
  41. package/dist/client/react.mjs +26 -3
  42. package/dist/client/react.mjs.map +1 -1
  43. package/dist/client/vue.js.map +1 -1
  44. package/dist/client/vue.mjs.map +1 -1
  45. package/dist/index.d.mts +1 -1
  46. package/dist/index.d.ts +1 -1
  47. package/dist/index.js +134 -71
  48. package/dist/index.js.map +1 -1
  49. package/dist/index.mjs +134 -71
  50. package/dist/index.mjs.map +1 -1
  51. package/dist/{multi-session-client-BYLarghq.d.ts → multi-session-client-CHE8QpVE.d.ts} +75 -5
  52. package/dist/{multi-session-client-CzhMkE0k.d.mts → multi-session-client-CQsRbxYI.d.mts} +75 -5
  53. package/dist/server/index.d.mts +1 -1
  54. package/dist/server/index.d.ts +1 -1
  55. package/dist/server/index.js +134 -71
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/index.mjs +134 -71
  58. package/dist/server/index.mjs.map +1 -1
  59. package/dist/shared/index.js +10 -2
  60. package/dist/shared/index.js.map +1 -1
  61. package/dist/shared/index.mjs +10 -2
  62. package/dist/shared/index.mjs.map +1 -1
  63. package/package.json +185 -185
  64. package/src/adapters/agui-adapter.ts +222 -222
  65. package/src/adapters/agui-middleware.ts +382 -382
  66. package/src/adapters/ai-adapter.ts +115 -115
  67. package/src/adapters/langchain-adapter.ts +127 -127
  68. package/src/adapters/mastra-adapter.ts +126 -126
  69. package/src/bin/mcp-ts.ts +102 -102
  70. package/src/client/core/app-host.ts +417 -417
  71. package/src/client/core/sse-client.ts +371 -371
  72. package/src/client/core/types.ts +31 -31
  73. package/src/client/index.ts +27 -27
  74. package/src/client/react/index.ts +16 -16
  75. package/src/client/react/use-app-host.ts +73 -73
  76. package/src/client/react/use-mcp-apps.tsx +247 -214
  77. package/src/client/react/use-mcp.ts +641 -641
  78. package/src/client/vue/index.ts +10 -10
  79. package/src/client/vue/use-mcp.ts +617 -617
  80. package/src/index.ts +11 -11
  81. package/src/server/handlers/nextjs-handler.ts +204 -204
  82. package/src/server/handlers/sse-handler.ts +631 -631
  83. package/src/server/index.ts +57 -57
  84. package/src/server/mcp/multi-session-client.ts +228 -132
  85. package/src/server/mcp/oauth-client.ts +1188 -1188
  86. package/src/server/mcp/storage-oauth-provider.ts +272 -272
  87. package/src/server/storage/file-backend.ts +157 -170
  88. package/src/server/storage/index.ts +176 -175
  89. package/src/server/storage/memory-backend.ts +123 -136
  90. package/src/server/storage/redis-backend.ts +276 -289
  91. package/src/server/storage/redis.ts +160 -160
  92. package/src/server/storage/sqlite-backend.ts +182 -186
  93. package/src/server/storage/supabase-backend.ts +228 -227
  94. package/src/server/storage/types.ts +116 -116
  95. package/src/shared/constants.ts +29 -29
  96. package/src/shared/errors.ts +133 -133
  97. package/src/shared/event-routing.ts +28 -28
  98. package/src/shared/events.ts +180 -180
  99. package/src/shared/index.ts +75 -75
  100. package/src/shared/tool-utils.ts +61 -61
  101. package/src/shared/types.ts +282 -282
  102. package/src/shared/utils.ts +38 -16
  103. package/supabase/migrations/20260330195700_install_mcp_sessions.sql +84 -84
@@ -367,25 +367,95 @@ interface MultiSessionOptions {
367
367
  retryDelay?: number;
368
368
  }
369
369
  /**
370
- * Manages multiple MCP connections for a single user identity.
371
- * Allows aggregating tools from all connected servers.
370
+ * Manages multiple MCP client connections for a single user identity.
371
+ *
372
+ * On a traditional long-running server, you can cache this instance per user
373
+ * so the connections stay alive between requests. On serverless, a new instance
374
+ * will be created per invocation, but the underlying session data is always
375
+ * read from the storage backend so nothing is lost between calls.
372
376
  */
373
377
  declare class MultiSessionClient {
374
378
  private clients;
375
379
  private identity;
376
380
  private options;
381
+ /**
382
+ * Creates a new MultiSessionClient for the given user identity.
383
+ *
384
+ * @param identity - A unique string identifying the user (e.g. user ID or email).
385
+ * @param options - Optional tuning for connection timeout, retry count, and retry delay.
386
+ * Falls back to sensible defaults if not provided.
387
+ */
377
388
  constructor(identity: string, options?: MultiSessionOptions);
389
+ /**
390
+ * Fetches all sessions for this identity from storage and returns only the
391
+ * ones that are ready to connect.
392
+ *
393
+ * A session is considered connectable when:
394
+ * - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
395
+ * - Its `active` flag is not explicitly `false` — sessions with `active: false` are
396
+ * either mid-OAuth flow, auth-pending, or previously failed. We skip those here
397
+ * and let the OAuth flow complete separately before we try to reconnect them.
398
+ *
399
+ * Note: Sessions where `active` is `undefined` (legacy records) are included
400
+ * for backwards compatibility.
401
+ */
378
402
  private getActiveSessions;
403
+ /**
404
+ * Connects to a list of sessions in controlled batches of `CONNECTION_BATCH_SIZE`.
405
+ *
406
+ * Batching prevents overwhelming the event loop or external servers when a user
407
+ * has many active MCP sessions (e.g. 20+ servers). Within each batch, sessions
408
+ * are connected concurrently using `Promise.all` for speed.
409
+ */
379
410
  private connectInBatches;
411
+ private connectionPromises;
412
+ /**
413
+ * Connects a single session, with built-in deduplication to prevent race conditions.
414
+ *
415
+ * - If a client for this session already exists and is connected, returns immediately.
416
+ * - If a connection attempt for this session is already in-flight (e.g. from a
417
+ * concurrent call), it joins the existing promise instead of starting a new one.
418
+ * This is the key concurrency lock — the `connectionPromises` map acts as a
419
+ * per-session mutex so we never spin up two physical connections for the same session.
420
+ * - On completion (success or failure), the promise is cleaned up from the map.
421
+ */
380
422
  private connectSession;
381
- private createAndConnectClient;
423
+ /**
424
+ * The core connection loop for a single session.
425
+ *
426
+ * Attempts to establish a physical MCP connection, retrying up to `maxRetries` times
427
+ * if the connection fails. Each attempt:
428
+ * 1. Creates a fresh `MCPClient` instance from the session data.
429
+ * 2. Races the connect call against a timeout promise — if the server doesn't respond
430
+ * within `timeoutMs`, the attempt is aborted and counted as a failure.
431
+ * 3. On success, replaces any stale client entry for this session in the `clients` array.
432
+ * 4. On failure, waits `retryDelay` ms before the next attempt.
433
+ *
434
+ * If all attempts are exhausted, logs an error and returns silently (does not throw),
435
+ * so a single bad server doesn't block the rest of the batch from connecting.
436
+ */
437
+ private establishConnectionWithRetries;
438
+ /**
439
+ * The main entry point. Fetches all active sessions for this identity from
440
+ * storage and establishes connections to all of them in batches.
441
+ *
442
+ * Call this once after creating the client. On traditional servers, you can
443
+ * cache the `MultiSessionClient` instance after calling `connect()` to avoid
444
+ * re-fetching and re-connecting on every request.
445
+ */
382
446
  connect(): Promise<void>;
383
447
  /**
384
- * Returns the array of currently connected clients.
448
+ * Returns all currently connected `MCPClient` instances.
449
+ *
450
+ * Use this to enumerate available tools across all connected servers,
451
+ * or to route a tool call to the right client by `serverId`.
385
452
  */
386
453
  getClients(): MCPClient[];
387
454
  /**
388
- * Disconnects all clients.
455
+ * Gracefully disconnects all active MCP clients and clears the internal client list.
456
+ *
457
+ * Call this during server shutdown or when a user logs out to free up
458
+ * underlying transport resources (SSE streams, HTTP connections, etc.).
389
459
  */
390
460
  disconnect(): void;
391
461
  }
@@ -367,25 +367,95 @@ interface MultiSessionOptions {
367
367
  retryDelay?: number;
368
368
  }
369
369
  /**
370
- * Manages multiple MCP connections for a single user identity.
371
- * Allows aggregating tools from all connected servers.
370
+ * Manages multiple MCP client connections for a single user identity.
371
+ *
372
+ * On a traditional long-running server, you can cache this instance per user
373
+ * so the connections stay alive between requests. On serverless, a new instance
374
+ * will be created per invocation, but the underlying session data is always
375
+ * read from the storage backend so nothing is lost between calls.
372
376
  */
373
377
  declare class MultiSessionClient {
374
378
  private clients;
375
379
  private identity;
376
380
  private options;
381
+ /**
382
+ * Creates a new MultiSessionClient for the given user identity.
383
+ *
384
+ * @param identity - A unique string identifying the user (e.g. user ID or email).
385
+ * @param options - Optional tuning for connection timeout, retry count, and retry delay.
386
+ * Falls back to sensible defaults if not provided.
387
+ */
377
388
  constructor(identity: string, options?: MultiSessionOptions);
389
+ /**
390
+ * Fetches all sessions for this identity from storage and returns only the
391
+ * ones that are ready to connect.
392
+ *
393
+ * A session is considered connectable when:
394
+ * - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
395
+ * - Its `active` flag is not explicitly `false` — sessions with `active: false` are
396
+ * either mid-OAuth flow, auth-pending, or previously failed. We skip those here
397
+ * and let the OAuth flow complete separately before we try to reconnect them.
398
+ *
399
+ * Note: Sessions where `active` is `undefined` (legacy records) are included
400
+ * for backwards compatibility.
401
+ */
378
402
  private getActiveSessions;
403
+ /**
404
+ * Connects to a list of sessions in controlled batches of `CONNECTION_BATCH_SIZE`.
405
+ *
406
+ * Batching prevents overwhelming the event loop or external servers when a user
407
+ * has many active MCP sessions (e.g. 20+ servers). Within each batch, sessions
408
+ * are connected concurrently using `Promise.all` for speed.
409
+ */
379
410
  private connectInBatches;
411
+ private connectionPromises;
412
+ /**
413
+ * Connects a single session, with built-in deduplication to prevent race conditions.
414
+ *
415
+ * - If a client for this session already exists and is connected, returns immediately.
416
+ * - If a connection attempt for this session is already in-flight (e.g. from a
417
+ * concurrent call), it joins the existing promise instead of starting a new one.
418
+ * This is the key concurrency lock — the `connectionPromises` map acts as a
419
+ * per-session mutex so we never spin up two physical connections for the same session.
420
+ * - On completion (success or failure), the promise is cleaned up from the map.
421
+ */
380
422
  private connectSession;
381
- private createAndConnectClient;
423
+ /**
424
+ * The core connection loop for a single session.
425
+ *
426
+ * Attempts to establish a physical MCP connection, retrying up to `maxRetries` times
427
+ * if the connection fails. Each attempt:
428
+ * 1. Creates a fresh `MCPClient` instance from the session data.
429
+ * 2. Races the connect call against a timeout promise — if the server doesn't respond
430
+ * within `timeoutMs`, the attempt is aborted and counted as a failure.
431
+ * 3. On success, replaces any stale client entry for this session in the `clients` array.
432
+ * 4. On failure, waits `retryDelay` ms before the next attempt.
433
+ *
434
+ * If all attempts are exhausted, logs an error and returns silently (does not throw),
435
+ * so a single bad server doesn't block the rest of the batch from connecting.
436
+ */
437
+ private establishConnectionWithRetries;
438
+ /**
439
+ * The main entry point. Fetches all active sessions for this identity from
440
+ * storage and establishes connections to all of them in batches.
441
+ *
442
+ * Call this once after creating the client. On traditional servers, you can
443
+ * cache the `MultiSessionClient` instance after calling `connect()` to avoid
444
+ * re-fetching and re-connecting on every request.
445
+ */
382
446
  connect(): Promise<void>;
383
447
  /**
384
- * Returns the array of currently connected clients.
448
+ * Returns all currently connected `MCPClient` instances.
449
+ *
450
+ * Use this to enumerate available tools across all connected servers,
451
+ * or to route a tool call to the right client by `serverId`.
385
452
  */
386
453
  getClients(): MCPClient[];
387
454
  /**
388
- * Disconnects all clients.
455
+ * Gracefully disconnects all active MCP clients and clears the internal client list.
456
+ *
457
+ * Call this during server shutdown or when a user logs out to free up
458
+ * underlying transport resources (SSE streams, HTTP connections, etc.).
389
459
  */
390
460
  disconnect(): void;
391
461
  }
@@ -1,4 +1,4 @@
1
- export { M as MCPClient, a as MultiSessionClient, S as StorageOAuthClientProvider } from '../multi-session-client-CzhMkE0k.mjs';
1
+ export { M as MCPClient, a as MultiSessionClient, S as StorageOAuthClientProvider } from '../multi-session-client-CQsRbxYI.mjs';
2
2
  export { U as UnauthorizedError, s as sanitizeServerLabel } from '../utils-0qmYrqoa.mjs';
3
3
  import { OAuthClientInformationMixed, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
4
4
  export { OAuthClientInformation, OAuthClientInformationFull, OAuthClientMetadata, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
@@ -1,4 +1,4 @@
1
- export { M as MCPClient, a as MultiSessionClient, S as StorageOAuthClientProvider } from '../multi-session-client-BYLarghq.js';
1
+ export { M as MCPClient, a as MultiSessionClient, S as StorageOAuthClientProvider } from '../multi-session-client-CHE8QpVE.js';
2
2
  export { U as UnauthorizedError, s as sanitizeServerLabel } from '../utils-0qmYrqoa.js';
3
3
  import { OAuthClientInformationMixed, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
4
4
  export { OAuthClientInformation, OAuthClientInformationFull, OAuthClientMetadata, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
@@ -169,7 +169,8 @@ var SOFTWARE_VERSION = "1.3.4";
169
169
  var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
170
170
  var MCP_CLIENT_VERSION = "2.0";
171
171
 
172
- // src/server/storage/redis-backend.ts
172
+ // src/shared/utils.ts
173
+ init_cjs_shims();
173
174
  var firstChar = nanoid.customAlphabet(
174
175
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
175
176
  1
@@ -178,6 +179,18 @@ var rest = nanoid.customAlphabet(
178
179
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
179
180
  11
180
181
  );
182
+ function sanitizeServerLabel(name) {
183
+ let sanitized = name.replace(/[^a-zA-Z0-9-_]/g, "_").replace(/_{2,}/g, "_").toLowerCase();
184
+ if (!/^[a-zA-Z]/.test(sanitized)) {
185
+ sanitized = "s_" + sanitized;
186
+ }
187
+ return sanitized;
188
+ }
189
+ function generateSessionId() {
190
+ return firstChar() + rest();
191
+ }
192
+
193
+ // src/server/storage/redis-backend.ts
181
194
  var RedisStorageBackend = class {
182
195
  constructor(redis2) {
183
196
  this.redis = redis2;
@@ -236,7 +249,7 @@ var RedisStorageBackend = class {
236
249
  return Array.from(keys);
237
250
  }
238
251
  generateSessionId() {
239
- return firstChar() + rest();
252
+ return generateSessionId();
240
253
  }
241
254
  async createSession(session, ttl) {
242
255
  const { sessionId, identity } = session;
@@ -407,14 +420,6 @@ var RedisStorageBackend = class {
407
420
 
408
421
  // src/server/storage/memory-backend.ts
409
422
  init_cjs_shims();
410
- var firstChar2 = nanoid.customAlphabet(
411
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
412
- 1
413
- );
414
- var rest2 = nanoid.customAlphabet(
415
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
416
- 11
417
- );
418
423
  var MemoryStorageBackend = class {
419
424
  constructor() {
420
425
  // Map<identity:sessionId, SessionData>
@@ -429,7 +434,7 @@ var MemoryStorageBackend = class {
429
434
  return `${identity}:${sessionId}`;
430
435
  }
431
436
  generateSessionId() {
432
- return firstChar2() + rest2();
437
+ return generateSessionId();
433
438
  }
434
439
  async createSession(session, ttl) {
435
440
  const { sessionId, identity } = session;
@@ -503,14 +508,6 @@ var MemoryStorageBackend = class {
503
508
 
504
509
  // src/server/storage/file-backend.ts
505
510
  init_cjs_shims();
506
- var firstChar3 = nanoid.customAlphabet(
507
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
508
- 1
509
- );
510
- var rest3 = nanoid.customAlphabet(
511
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
512
- 11
513
- );
514
511
  var FileStorageBackend = class {
515
512
  /**
516
513
  * @param options.path Path to the JSON file storage (default: ./sessions.json)
@@ -561,7 +558,7 @@ var FileStorageBackend = class {
561
558
  return `${identity}:${sessionId}`;
562
559
  }
563
560
  generateSessionId() {
564
- return firstChar3() + rest3();
561
+ return generateSessionId();
565
562
  }
566
563
  async createSession(session, ttl) {
567
564
  await this.ensureInitialized();
@@ -671,12 +668,7 @@ var SqliteStorage = class {
671
668
  }
672
669
  }
673
670
  generateSessionId() {
674
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
675
- let result = "";
676
- for (let i = 0; i < 32; i++) {
677
- result += chars.charAt(Math.floor(Math.random() * chars.length));
678
- }
679
- return result;
671
+ return generateSessionId();
680
672
  }
681
673
  async createSession(session, ttl) {
682
674
  this.ensureInitialized();
@@ -791,7 +783,7 @@ var SupabaseStorageBackend = class {
791
783
  console.log('[mcp-ts][Storage] Supabase: \u2713 "mcp_sessions" table verified.');
792
784
  }
793
785
  generateSessionId() {
794
- return crypto.randomUUID();
786
+ return generateSessionId();
795
787
  }
796
788
  mapRowToSessionData(row) {
797
789
  return {
@@ -1232,16 +1224,6 @@ var StorageOAuthClientProvider = class {
1232
1224
  }
1233
1225
  };
1234
1226
 
1235
- // src/shared/utils.ts
1236
- init_cjs_shims();
1237
- function sanitizeServerLabel(name) {
1238
- let sanitized = name.replace(/[^a-zA-Z0-9-_]/g, "_").replace(/_{2,}/g, "_").toLowerCase();
1239
- if (!/^[a-zA-Z]/.test(sanitized)) {
1240
- sanitized = "s_" + sanitized;
1241
- }
1242
- return sanitized;
1243
- }
1244
-
1245
1227
  // src/shared/events.ts
1246
1228
  init_cjs_shims();
1247
1229
  var Emitter = class {
@@ -2207,47 +2189,132 @@ var MCPClient = class _MCPClient {
2207
2189
 
2208
2190
  // src/server/mcp/multi-session-client.ts
2209
2191
  init_cjs_shims();
2192
+ var DEFAULT_TIMEOUT_MS = 15e3;
2193
+ var DEFAULT_MAX_RETRIES = 2;
2194
+ var DEFAULT_RETRY_DELAY_MS = 1e3;
2195
+ var CONNECTION_BATCH_SIZE = 5;
2210
2196
  var MultiSessionClient = class {
2197
+ /**
2198
+ * Creates a new MultiSessionClient for the given user identity.
2199
+ *
2200
+ * @param identity - A unique string identifying the user (e.g. user ID or email).
2201
+ * @param options - Optional tuning for connection timeout, retry count, and retry delay.
2202
+ * Falls back to sensible defaults if not provided.
2203
+ */
2211
2204
  constructor(identity, options = {}) {
2212
2205
  __publicField(this, "clients", []);
2213
2206
  __publicField(this, "identity");
2214
2207
  __publicField(this, "options");
2208
+ __publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
2215
2209
  this.identity = identity;
2216
2210
  this.options = {
2217
- timeout: 15e3,
2218
- maxRetries: 2,
2219
- retryDelay: 1e3,
2211
+ timeout: DEFAULT_TIMEOUT_MS,
2212
+ maxRetries: DEFAULT_MAX_RETRIES,
2213
+ retryDelay: DEFAULT_RETRY_DELAY_MS,
2220
2214
  ...options
2221
2215
  };
2222
2216
  }
2217
+ /**
2218
+ * Fetches all sessions for this identity from storage and returns only the
2219
+ * ones that are ready to connect.
2220
+ *
2221
+ * A session is considered connectable when:
2222
+ * - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
2223
+ * - Its `active` flag is not explicitly `false` — sessions with `active: false` are
2224
+ * either mid-OAuth flow, auth-pending, or previously failed. We skip those here
2225
+ * and let the OAuth flow complete separately before we try to reconnect them.
2226
+ *
2227
+ * Note: Sessions where `active` is `undefined` (legacy records) are included
2228
+ * for backwards compatibility.
2229
+ */
2223
2230
  async getActiveSessions() {
2224
2231
  const sessions = await storage.getIdentitySessionsData(this.identity);
2225
- console.log(
2226
- `[MultiSessionClient] All sessions for ${this.identity}:`,
2227
- sessions.map((s) => ({ sessionId: s.sessionId, serverId: s.serverId }))
2232
+ const valid = sessions.filter(
2233
+ (s) => s.serverId && s.serverUrl && s.callbackUrl && s.active !== false
2234
+ // exclude OAuth-pending / failed sessions
2228
2235
  );
2229
- const valid = sessions.filter((s) => s.serverId && s.serverUrl && s.callbackUrl);
2230
- console.log(`[MultiSessionClient] Filtered valid sessions:`, valid.length);
2231
2236
  return valid;
2232
2237
  }
2238
+ /**
2239
+ * Connects to a list of sessions in controlled batches of `CONNECTION_BATCH_SIZE`.
2240
+ *
2241
+ * Batching prevents overwhelming the event loop or external servers when a user
2242
+ * has many active MCP sessions (e.g. 20+ servers). Within each batch, sessions
2243
+ * are connected concurrently using `Promise.all` for speed.
2244
+ */
2233
2245
  async connectInBatches(sessions) {
2234
- const BATCH_SIZE = 5;
2235
- for (let i = 0; i < sessions.length; i += BATCH_SIZE) {
2236
- const batch = sessions.slice(i, i + BATCH_SIZE);
2246
+ for (let i = 0; i < sessions.length; i += CONNECTION_BATCH_SIZE) {
2247
+ const batch = sessions.slice(i, i + CONNECTION_BATCH_SIZE);
2237
2248
  await Promise.all(batch.map((session) => this.connectSession(session)));
2238
2249
  }
2239
2250
  }
2251
+ /**
2252
+ * Connects a single session, with built-in deduplication to prevent race conditions.
2253
+ *
2254
+ * - If a client for this session already exists and is connected, returns immediately.
2255
+ * - If a connection attempt for this session is already in-flight (e.g. from a
2256
+ * concurrent call), it joins the existing promise instead of starting a new one.
2257
+ * This is the key concurrency lock — the `connectionPromises` map acts as a
2258
+ * per-session mutex so we never spin up two physical connections for the same session.
2259
+ * - On completion (success or failure), the promise is cleaned up from the map.
2260
+ */
2240
2261
  async connectSession(session) {
2241
2262
  const existingClient = this.clients.find((c) => c.getSessionId() === session.sessionId);
2242
2263
  if (existingClient?.isConnected()) {
2243
2264
  return;
2244
2265
  }
2245
- const maxRetries = this.options.maxRetries ?? 2;
2246
- const retryDelay = this.options.retryDelay ?? 1e3;
2266
+ if (this.connectionPromises.has(session.sessionId)) {
2267
+ return this.connectionPromises.get(session.sessionId);
2268
+ }
2269
+ const connectPromise = this.establishConnectionWithRetries(session);
2270
+ this.connectionPromises.set(session.sessionId, connectPromise);
2271
+ try {
2272
+ await connectPromise;
2273
+ } finally {
2274
+ this.connectionPromises.delete(session.sessionId);
2275
+ }
2276
+ }
2277
+ /**
2278
+ * The core connection loop for a single session.
2279
+ *
2280
+ * Attempts to establish a physical MCP connection, retrying up to `maxRetries` times
2281
+ * if the connection fails. Each attempt:
2282
+ * 1. Creates a fresh `MCPClient` instance from the session data.
2283
+ * 2. Races the connect call against a timeout promise — if the server doesn't respond
2284
+ * within `timeoutMs`, the attempt is aborted and counted as a failure.
2285
+ * 3. On success, replaces any stale client entry for this session in the `clients` array.
2286
+ * 4. On failure, waits `retryDelay` ms before the next attempt.
2287
+ *
2288
+ * If all attempts are exhausted, logs an error and returns silently (does not throw),
2289
+ * so a single bad server doesn't block the rest of the batch from connecting.
2290
+ */
2291
+ async establishConnectionWithRetries(session) {
2292
+ const maxRetries = this.options.maxRetries ?? DEFAULT_MAX_RETRIES;
2293
+ const retryDelay = this.options.retryDelay ?? DEFAULT_RETRY_DELAY_MS;
2247
2294
  let lastError;
2248
2295
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
2249
2296
  try {
2250
- const client = await this.createAndConnectClient(session);
2297
+ const client = new MCPClient({
2298
+ identity: this.identity,
2299
+ sessionId: session.sessionId,
2300
+ serverId: session.serverId,
2301
+ serverUrl: session.serverUrl,
2302
+ callbackUrl: session.callbackUrl,
2303
+ serverName: session.serverName,
2304
+ transportType: session.transportType,
2305
+ headers: session.headers
2306
+ });
2307
+ const timeoutMs = this.options.timeout ?? DEFAULT_TIMEOUT_MS;
2308
+ let timeoutTimer;
2309
+ const timeoutPromise = new Promise((_, reject) => {
2310
+ timeoutTimer = setTimeout(() => reject(new Error(`Connection timed out after ${timeoutMs}ms`)), timeoutMs);
2311
+ });
2312
+ try {
2313
+ await Promise.race([client.connect(), timeoutPromise]);
2314
+ } finally {
2315
+ clearTimeout(timeoutTimer);
2316
+ }
2317
+ this.clients = this.clients.filter((c) => c.getSessionId() !== session.sessionId);
2251
2318
  this.clients.push(client);
2252
2319
  return;
2253
2320
  } catch (error) {
@@ -2259,36 +2326,32 @@ var MultiSessionClient = class {
2259
2326
  }
2260
2327
  console.error(`[MultiSessionClient] Failed to connect to session ${session.sessionId} after ${maxRetries + 1} attempts:`, lastError);
2261
2328
  }
2262
- async createAndConnectClient(session) {
2263
- const client = new MCPClient({
2264
- identity: this.identity,
2265
- sessionId: session.sessionId,
2266
- serverId: session.serverId,
2267
- serverUrl: session.serverUrl,
2268
- callbackUrl: session.callbackUrl,
2269
- serverName: session.serverName,
2270
- transportType: session.transportType,
2271
- headers: session.headers
2272
- });
2273
- const timeoutMs = this.options.timeout ?? 15e3;
2274
- const timeoutPromise = new Promise((_, reject) => {
2275
- setTimeout(() => reject(new Error(`Connection timed out after ${timeoutMs}ms`)), timeoutMs);
2276
- });
2277
- await Promise.race([client.connect(), timeoutPromise]);
2278
- return client;
2279
- }
2329
+ /**
2330
+ * The main entry point. Fetches all active sessions for this identity from
2331
+ * storage and establishes connections to all of them in batches.
2332
+ *
2333
+ * Call this once after creating the client. On traditional servers, you can
2334
+ * cache the `MultiSessionClient` instance after calling `connect()` to avoid
2335
+ * re-fetching and re-connecting on every request.
2336
+ */
2280
2337
  async connect() {
2281
2338
  const sessions = await this.getActiveSessions();
2282
2339
  await this.connectInBatches(sessions);
2283
2340
  }
2284
2341
  /**
2285
- * Returns the array of currently connected clients.
2342
+ * Returns all currently connected `MCPClient` instances.
2343
+ *
2344
+ * Use this to enumerate available tools across all connected servers,
2345
+ * or to route a tool call to the right client by `serverId`.
2286
2346
  */
2287
2347
  getClients() {
2288
2348
  return this.clients;
2289
2349
  }
2290
2350
  /**
2291
- * Disconnects all clients.
2351
+ * Gracefully disconnects all active MCP clients and clears the internal client list.
2352
+ *
2353
+ * Call this during server shutdown or when a user logs out to free up
2354
+ * underlying transport resources (SSE streams, HTTP connections, etc.).
2292
2355
  */
2293
2356
  disconnect() {
2294
2357
  this.clients.forEach((client) => client.disconnect());