@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
package/dist/index.js CHANGED
@@ -2363,7 +2363,7 @@ var MCPClient = class {
2363
2363
  * Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
2364
2364
  * This wrapper enforces a timeout and properly uses AbortController to unblock the request.
2365
2365
  */
2366
- fetch: (url, init) => {
2366
+ fetch: async (url, init) => {
2367
2367
  const timeout = 3e4;
2368
2368
  const controller = new AbortController();
2369
2369
  const timeoutId = setTimeout(() => controller.abort(), timeout);
@@ -2371,7 +2371,17 @@ var MCPClient = class {
2371
2371
  // @ts-ignore: AbortSignal.any is available in Node 20+
2372
2372
  AbortSignal.any ? AbortSignal.any([init.signal, controller.signal]) : controller.signal
2373
2373
  ) : controller.signal;
2374
- return fetch(url, { ...init, signal }).finally(() => clearTimeout(timeoutId));
2374
+ try {
2375
+ const response = await fetch(url, { ...init, signal });
2376
+ const hasSessionHeader = init?.headers && new Headers(init.headers).has("mcp-session-id");
2377
+ if (response.status === 404 && hasSessionHeader) {
2378
+ this.client = null;
2379
+ throw new Error("MCP_SESSION_EXPIRED: Downstream session was not found on the server.");
2380
+ }
2381
+ return response;
2382
+ } finally {
2383
+ clearTimeout(timeoutId);
2384
+ }
2375
2385
  }
2376
2386
  };
2377
2387
  if (type === "sse") {
@@ -2563,6 +2573,14 @@ var MCPClient = class {
2563
2573
  * @throws {Error} When connection fails for other reasons
2564
2574
  */
2565
2575
  async connect() {
2576
+ if (this.client?.transport) {
2577
+ this.transport = null;
2578
+ try {
2579
+ await this.client.close();
2580
+ } catch {
2581
+ }
2582
+ this.client = null;
2583
+ }
2566
2584
  await this.initialize();
2567
2585
  if (!this.client || !this.oauthProvider) {
2568
2586
  const error = "Client or OAuth provider not initialized";
@@ -2941,7 +2959,7 @@ var MCPClient = class {
2941
2959
  await this.oauthProvider.invalidateCredentials("all");
2942
2960
  }
2943
2961
  await sessions.delete(this.userId, this.sessionId);
2944
- this.disconnect();
2962
+ await this.disconnect();
2945
2963
  }
2946
2964
  /**
2947
2965
  * Checks if the client is currently connected to an MCP server
@@ -2951,10 +2969,21 @@ var MCPClient = class {
2951
2969
  return this.client !== null;
2952
2970
  }
2953
2971
  /**
2954
- * Disconnects from the MCP server and cleans up resources
2955
- * Does not remove session from Redis - use clearSession() for that
2972
+ * Disconnects from the MCP server and cleans up resources.
2973
+ * Does not remove session from Redis use clearSession() for that.
2974
+ *
2975
+ * For Streamable HTTP sessions, sends an HTTP DELETE to the MCP endpoint
2976
+ * before closing, as recommended by the MCP Streamable HTTP spec
2977
+ * (section "Session Management", rule 5). This is best-effort — errors
2978
+ * (e.g. server already restarted, 404/405 responses) are silently ignored.
2956
2979
  */
2957
- disconnect(reason) {
2980
+ async disconnect() {
2981
+ if (this.transport instanceof streamableHttp_js.StreamableHTTPClientTransport) {
2982
+ try {
2983
+ await this.transport.terminateSession();
2984
+ } catch {
2985
+ }
2986
+ }
2958
2987
  if (this.client) {
2959
2988
  this.client.close();
2960
2989
  }
@@ -2966,7 +2995,6 @@ var MCPClient = class {
2966
2995
  type: "disconnected",
2967
2996
  sessionId: this.sessionId,
2968
2997
  serverId: this.serverId,
2969
- reason,
2970
2998
  timestamp: Date.now()
2971
2999
  });
2972
3000
  this._onObservabilityEvent.fire({
@@ -2975,9 +3003,7 @@ var MCPClient = class {
2975
3003
  message: `Disconnected from ${this.serverId}`,
2976
3004
  sessionId: this.sessionId,
2977
3005
  serverId: this.serverId,
2978
- payload: {
2979
- reason: reason || "unknown"
2980
- },
3006
+ payload: {},
2981
3007
  timestamp: Date.now(),
2982
3008
  id: nanoid.nanoid()
2983
3009
  });
@@ -3044,17 +3070,14 @@ var DEFAULT_RETRY_DELAY_MS = 1e3;
3044
3070
  var CONNECTION_BATCH_SIZE = 5;
3045
3071
  var MultiSessionClient = class {
3046
3072
  /**
3047
- * Creates a new MultiSessionClient for the given user userId.
3048
- *
3049
- * @param userId - A unique string identifying the user (e.g. user ID or email).
3050
- * @param options - Optional tuning for connection timeout, retry count, and retry delay.
3051
- * Falls back to sensible defaults if not provided.
3073
+ * @param userId - Unique identifier for the user (e.g. user ID or email).
3074
+ * @param options - Optional tuning and lifecycle hooks.
3052
3075
  */
3053
3076
  constructor(userId, options = {}) {
3054
3077
  __publicField(this, "clients", []);
3078
+ __publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
3055
3079
  __publicField(this, "userId");
3056
3080
  __publicField(this, "options");
3057
- __publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
3058
3081
  this.userId = userId;
3059
3082
  this.options = {
3060
3083
  timeout: DEFAULT_TIMEOUT_MS,
@@ -3063,28 +3086,85 @@ var MultiSessionClient = class {
3063
3086
  ...options
3064
3087
  };
3065
3088
  }
3089
+ // -----------------------------------------------------------------------
3090
+ // Public API
3091
+ // -----------------------------------------------------------------------
3066
3092
  /**
3067
- * Fetches all sessions for this userId from storage and returns only the
3068
- * ones that are ready to connect.
3093
+ * Fetches active sessions and establishes connections to all of them.
3069
3094
  *
3070
- * A session is considered connectable when:
3071
- * - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
3072
- * - Its status is `active`. Pending sessions are skipped here
3073
- * and let the OAuth flow complete separately before we try to reconnect them.
3095
+ * Call this once after creating the client. On long-running servers you
3096
+ * can cache the `MultiSessionClient` instance and call `connect()` on
3097
+ * each request already-connected sessions are skipped internally.
3074
3098
  */
3075
- async getActiveSessions() {
3076
- const sessionList = await sessions.list(this.userId);
3077
- const valid = sessionList.filter(
3099
+ async connect() {
3100
+ const sessions2 = await this.fetchActiveSessions();
3101
+ const activeSessionIds = new Set(sessions2.map((s) => s.sessionId));
3102
+ for (const client of this.clients) {
3103
+ if (!activeSessionIds.has(client.getSessionId())) {
3104
+ this.options.onSessionEvicted?.(client.getSessionId());
3105
+ }
3106
+ }
3107
+ this.clients = this.clients.filter((c) => activeSessionIds.has(c.getSessionId()));
3108
+ await this.connectInBatches(sessions2);
3109
+ }
3110
+ /**
3111
+ * Drops all cached `MCPClient` instances and reconnects fresh from storage.
3112
+ *
3113
+ * Call this when downstream MCP servers have expired their transport sessions
3114
+ * (e.g. after a remote server restart) and subsequent tool calls return
3115
+ * "Session not found. Reconnect without session header." errors.
3116
+ *
3117
+ * OAuth tokens are preserved in the storage backend — no re-authentication
3118
+ * is required. Only the in-memory transport sessions are cleared.
3119
+ */
3120
+ async reconnect() {
3121
+ await this.disconnect();
3122
+ await this.connect();
3123
+ }
3124
+ /**
3125
+ * Returns all currently connected `MCPClient` instances.
3126
+ *
3127
+ * Use this to enumerate available tools across all connected servers,
3128
+ * or to route a tool call to the right client by `serverId`.
3129
+ */
3130
+ getClients() {
3131
+ return this.clients;
3132
+ }
3133
+ /**
3134
+ * Gracefully disconnects all active MCP clients and clears the internal list.
3135
+ *
3136
+ * For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
3137
+ * endpoint per the spec before closing locally. All disconnects run in
3138
+ * parallel so shutdown is not serialised across many sessions.
3139
+ *
3140
+ * Call this during server shutdown or when a user logs out to free up
3141
+ * underlying transport resources (SSE streams, HTTP connections, etc.).
3142
+ */
3143
+ async disconnect() {
3144
+ await Promise.all(this.clients.map((client) => client.disconnect()));
3145
+ this.clients = [];
3146
+ }
3147
+ // -----------------------------------------------------------------------
3148
+ // Internals
3149
+ // -----------------------------------------------------------------------
3150
+ /**
3151
+ * Resolves the list of sessions to connect.
3152
+ *
3153
+ * Uses the custom `sessionProvider` when provided, otherwise falls back
3154
+ * to querying the storage backend via `sessions.list(userId)`.
3155
+ */
3156
+ async fetchActiveSessions() {
3157
+ const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await sessions.list(this.userId);
3158
+ return sessionList.filter(
3078
3159
  (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
3079
3160
  );
3080
- return valid;
3081
3161
  }
3082
3162
  /**
3083
- * Connects to a list of sessions in controlled batches of `CONNECTION_BATCH_SIZE`.
3163
+ * Connects a list of sessions in controlled batches.
3084
3164
  *
3085
- * Batching prevents overwhelming the event loop or external servers when a user
3086
- * has many active MCP sessions (e.g. 20+ servers). Within each batch, sessions
3087
- * are connected concurrently using `Promise.all` for speed.
3165
+ * Batching prevents overwhelming the event loop or external servers when
3166
+ * a user has many active MCP sessions. Within each batch, sessions are
3167
+ * connected concurrently using `Promise.all`.
3088
3168
  */
3089
3169
  async connectInBatches(sessions2) {
3090
3170
  for (let i = 0; i < sessions2.length; i += CONNECTION_BATCH_SIZE) {
@@ -3093,19 +3173,25 @@ var MultiSessionClient = class {
3093
3173
  }
3094
3174
  }
3095
3175
  /**
3096
- * Connects a single session, with built-in deduplication to prevent race conditions.
3176
+ * Connects a single session, with deduplication to prevent race conditions.
3097
3177
  *
3098
- * - If a client for this session already exists and is connected, returns immediately.
3099
- * - If a connection attempt for this session is already in-flight (e.g. from a
3100
- * concurrent call), it joins the existing promise instead of starting a new one.
3101
- * This is the key concurrency lock the `connectionPromises` map acts as a
3102
- * per-session mutex so we never spin up two physical connections for the same session.
3103
- * - On completion (success or failure), the promise is cleaned up from the map.
3178
+ * - If a client for this session already exists and is connected, returns
3179
+ * immediately.
3180
+ * - If the existing client entry is no longer connected (e.g. explicit
3181
+ * disconnect), it is evicted so a fresh transport is created.
3182
+ * - If a connection attempt for this session is already in-flight, the
3183
+ * existing promise is reused as a per-session mutex.
3184
+ * - On completion (success or failure), the promise is cleaned up from
3185
+ * the connectionPromises map.
3104
3186
  */
3105
3187
  async connectSession(session) {
3106
- const existingClient = this.clients.find((c) => c.getSessionId() === session.sessionId);
3107
- if (existingClient?.isConnected()) {
3108
- return;
3188
+ const existing = this.clients.find((c) => c.getSessionId() === session.sessionId);
3189
+ if (existing) {
3190
+ if (existing.isConnected()) {
3191
+ return;
3192
+ }
3193
+ this.options.onSessionEvicted?.(existing.getSessionId());
3194
+ this.clients = this.clients.filter((c) => c !== existing);
3109
3195
  }
3110
3196
  if (this.connectionPromises.has(session.sessionId)) {
3111
3197
  return this.connectionPromises.get(session.sessionId);
@@ -3119,22 +3205,19 @@ var MultiSessionClient = class {
3119
3205
  }
3120
3206
  }
3121
3207
  /**
3122
- * The core connection loop for a single session.
3208
+ * Core connection loop for a single session with retry logic.
3123
3209
  *
3124
- * Attempts to establish a physical MCP connection, retrying up to `maxRetries` times
3125
- * if the connection fails. Each attempt:
3126
- * 1. Creates a fresh `MCPClient` instance from the session data.
3127
- * 2. Races the connect call against a timeout promise — if the server doesn't respond
3128
- * within `timeoutMs`, the attempt is aborted and counted as a failure.
3129
- * 3. On success, replaces any stale client entry for this session in the `clients` array.
3210
+ * 1. Creates a fresh `MCPClient` from the session data.
3211
+ * 2. Races `client.connect()` against a timeout.
3212
+ * 3. On success, replaces any stale entry and fires `onSessionConnected`.
3130
3213
  * 4. On failure, waits `retryDelay` ms before the next attempt.
3131
3214
  *
3132
- * If all attempts are exhausted, logs an error and returns silently (does not throw),
3133
- * so a single bad server doesn't block the rest of the batch from connecting.
3215
+ * If all attempts are exhausted, logs an error and returns silently so
3216
+ * a single bad server doesn't block the rest of the batch.
3134
3217
  */
3135
3218
  async establishConnectionWithRetries(session) {
3136
- const maxRetries = this.options.maxRetries ?? DEFAULT_MAX_RETRIES;
3137
- const retryDelay = this.options.retryDelay ?? DEFAULT_RETRY_DELAY_MS;
3219
+ const maxRetries = this.options.maxRetries;
3220
+ const retryDelay = this.options.retryDelay;
3138
3221
  let lastError;
3139
3222
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
3140
3223
  try {
@@ -3148,10 +3231,13 @@ var MultiSessionClient = class {
3148
3231
  transportType: session.transportType,
3149
3232
  headers: session.headers
3150
3233
  });
3151
- const timeoutMs = this.options.timeout ?? DEFAULT_TIMEOUT_MS;
3234
+ const timeoutMs = this.options.timeout;
3152
3235
  let timeoutTimer;
3153
3236
  const timeoutPromise = new Promise((_, reject) => {
3154
- timeoutTimer = setTimeout(() => reject(new Error(`Connection timed out after ${timeoutMs}ms`)), timeoutMs);
3237
+ timeoutTimer = setTimeout(
3238
+ () => reject(new Error(`Connection timed out after ${timeoutMs}ms`)),
3239
+ timeoutMs
3240
+ );
3155
3241
  });
3156
3242
  try {
3157
3243
  await Promise.race([client.connect(), timeoutPromise]);
@@ -3160,6 +3246,7 @@ var MultiSessionClient = class {
3160
3246
  }
3161
3247
  this.clients = this.clients.filter((c) => c.getSessionId() !== session.sessionId);
3162
3248
  this.clients.push(client);
3249
+ this.options.onSessionConnected?.(session.sessionId, client);
3163
3250
  return;
3164
3251
  } catch (error) {
3165
3252
  lastError = error;
@@ -3168,38 +3255,11 @@ var MultiSessionClient = class {
3168
3255
  }
3169
3256
  }
3170
3257
  }
3171
- console.error(`[MultiSessionClient] Failed to connect to session ${session.sessionId} after ${maxRetries + 1} attempts:`, lastError);
3172
- }
3173
- /**
3174
- * The main entry point. Fetches all active sessions for this userId from
3175
- * storage and establishes connections to all of them in batches.
3176
- *
3177
- * Call this once after creating the client. On traditional servers, you can
3178
- * cache the `MultiSessionClient` instance after calling `connect()` to avoid
3179
- * re-fetching and re-connecting on every request.
3180
- */
3181
- async connect() {
3182
- const sessions2 = await this.getActiveSessions();
3183
- await this.connectInBatches(sessions2);
3184
- }
3185
- /**
3186
- * Returns all currently connected `MCPClient` instances.
3187
- *
3188
- * Use this to enumerate available tools across all connected servers,
3189
- * or to route a tool call to the right client by `serverId`.
3190
- */
3191
- getClients() {
3192
- return this.clients;
3193
- }
3194
- /**
3195
- * Gracefully disconnects all active MCP clients and clears the internal client list.
3196
- *
3197
- * Call this during server shutdown or when a user logs out to free up
3198
- * underlying transport resources (SSE streams, HTTP connections, etc.).
3199
- */
3200
- disconnect() {
3201
- this.clients.forEach((client) => client.disconnect());
3202
- this.clients = [];
3258
+ this.options.onSessionFailed?.(session.sessionId, lastError);
3259
+ console.error(
3260
+ `[MultiSessionClient] Failed to connect to session ${session.sessionId} after ${maxRetries + 1} attempts:`,
3261
+ lastError
3262
+ );
3203
3263
  }
3204
3264
  };
3205
3265
 
@@ -3432,7 +3492,6 @@ var SSEConnectionManager = class {
3432
3492
  const client = this.clients.get(sessionId);
3433
3493
  if (client) {
3434
3494
  await client.clearSession();
3435
- client.disconnect();
3436
3495
  this.clients.delete(sessionId);
3437
3496
  } else {
3438
3497
  await sessions.delete(this.userId, sessionId);
@@ -3641,14 +3700,14 @@ var SSEConnectionManager = class {
3641
3700
  /**
3642
3701
  * Cleanup and close all connections
3643
3702
  */
3644
- dispose() {
3703
+ async dispose() {
3645
3704
  this.isActive = false;
3646
3705
  if (this.heartbeatTimer) {
3647
3706
  clearInterval(this.heartbeatTimer);
3648
3707
  }
3649
- for (const client of this.clients.values()) {
3650
- client.disconnect();
3651
- }
3708
+ await Promise.all(
3709
+ Array.from(this.clients.values()).map((client) => client.disconnect())
3710
+ );
3652
3711
  this.clients.clear();
3653
3712
  }
3654
3713
  };