@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
@@ -2288,7 +2288,7 @@ var MCPClient = class {
2288
2288
  * Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
2289
2289
  * This wrapper enforces a timeout and properly uses AbortController to unblock the request.
2290
2290
  */
2291
- fetch: (url, init) => {
2291
+ fetch: async (url, init) => {
2292
2292
  const timeout = 3e4;
2293
2293
  const controller = new AbortController();
2294
2294
  const timeoutId = setTimeout(() => controller.abort(), timeout);
@@ -2296,7 +2296,17 @@ var MCPClient = class {
2296
2296
  // @ts-ignore: AbortSignal.any is available in Node 20+
2297
2297
  AbortSignal.any ? AbortSignal.any([init.signal, controller.signal]) : controller.signal
2298
2298
  ) : controller.signal;
2299
- return fetch(url, { ...init, signal }).finally(() => clearTimeout(timeoutId));
2299
+ try {
2300
+ const response = await fetch(url, { ...init, signal });
2301
+ const hasSessionHeader = init?.headers && new Headers(init.headers).has("mcp-session-id");
2302
+ if (response.status === 404 && hasSessionHeader) {
2303
+ this.client = null;
2304
+ throw new Error("MCP_SESSION_EXPIRED: Downstream session was not found on the server.");
2305
+ }
2306
+ return response;
2307
+ } finally {
2308
+ clearTimeout(timeoutId);
2309
+ }
2300
2310
  }
2301
2311
  };
2302
2312
  if (type === "sse") {
@@ -2488,6 +2498,14 @@ var MCPClient = class {
2488
2498
  * @throws {Error} When connection fails for other reasons
2489
2499
  */
2490
2500
  async connect() {
2501
+ if (this.client?.transport) {
2502
+ this.transport = null;
2503
+ try {
2504
+ await this.client.close();
2505
+ } catch {
2506
+ }
2507
+ this.client = null;
2508
+ }
2491
2509
  await this.initialize();
2492
2510
  if (!this.client || !this.oauthProvider) {
2493
2511
  const error = "Client or OAuth provider not initialized";
@@ -2866,7 +2884,7 @@ var MCPClient = class {
2866
2884
  await this.oauthProvider.invalidateCredentials("all");
2867
2885
  }
2868
2886
  await sessions.delete(this.userId, this.sessionId);
2869
- this.disconnect();
2887
+ await this.disconnect();
2870
2888
  }
2871
2889
  /**
2872
2890
  * Checks if the client is currently connected to an MCP server
@@ -2876,10 +2894,21 @@ var MCPClient = class {
2876
2894
  return this.client !== null;
2877
2895
  }
2878
2896
  /**
2879
- * Disconnects from the MCP server and cleans up resources
2880
- * Does not remove session from Redis - use clearSession() for that
2897
+ * Disconnects from the MCP server and cleans up resources.
2898
+ * Does not remove session from Redis use clearSession() for that.
2899
+ *
2900
+ * For Streamable HTTP sessions, sends an HTTP DELETE to the MCP endpoint
2901
+ * before closing, as recommended by the MCP Streamable HTTP spec
2902
+ * (section "Session Management", rule 5). This is best-effort — errors
2903
+ * (e.g. server already restarted, 404/405 responses) are silently ignored.
2881
2904
  */
2882
- disconnect(reason) {
2905
+ async disconnect() {
2906
+ if (this.transport instanceof streamableHttp_js.StreamableHTTPClientTransport) {
2907
+ try {
2908
+ await this.transport.terminateSession();
2909
+ } catch {
2910
+ }
2911
+ }
2883
2912
  if (this.client) {
2884
2913
  this.client.close();
2885
2914
  }
@@ -2891,7 +2920,6 @@ var MCPClient = class {
2891
2920
  type: "disconnected",
2892
2921
  sessionId: this.sessionId,
2893
2922
  serverId: this.serverId,
2894
- reason,
2895
2923
  timestamp: Date.now()
2896
2924
  });
2897
2925
  this._onObservabilityEvent.fire({
@@ -2900,9 +2928,7 @@ var MCPClient = class {
2900
2928
  message: `Disconnected from ${this.serverId}`,
2901
2929
  sessionId: this.sessionId,
2902
2930
  serverId: this.serverId,
2903
- payload: {
2904
- reason: reason || "unknown"
2905
- },
2931
+ payload: {},
2906
2932
  timestamp: Date.now(),
2907
2933
  id: nanoid.nanoid()
2908
2934
  });
@@ -2969,17 +2995,14 @@ var DEFAULT_RETRY_DELAY_MS = 1e3;
2969
2995
  var CONNECTION_BATCH_SIZE = 5;
2970
2996
  var MultiSessionClient = class {
2971
2997
  /**
2972
- * Creates a new MultiSessionClient for the given user userId.
2973
- *
2974
- * @param userId - A unique string identifying the user (e.g. user ID or email).
2975
- * @param options - Optional tuning for connection timeout, retry count, and retry delay.
2976
- * Falls back to sensible defaults if not provided.
2998
+ * @param userId - Unique identifier for the user (e.g. user ID or email).
2999
+ * @param options - Optional tuning and lifecycle hooks.
2977
3000
  */
2978
3001
  constructor(userId, options = {}) {
2979
3002
  __publicField(this, "clients", []);
3003
+ __publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
2980
3004
  __publicField(this, "userId");
2981
3005
  __publicField(this, "options");
2982
- __publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
2983
3006
  this.userId = userId;
2984
3007
  this.options = {
2985
3008
  timeout: DEFAULT_TIMEOUT_MS,
@@ -2988,28 +3011,85 @@ var MultiSessionClient = class {
2988
3011
  ...options
2989
3012
  };
2990
3013
  }
3014
+ // -----------------------------------------------------------------------
3015
+ // Public API
3016
+ // -----------------------------------------------------------------------
2991
3017
  /**
2992
- * Fetches all sessions for this userId from storage and returns only the
2993
- * ones that are ready to connect.
3018
+ * Fetches active sessions and establishes connections to all of them.
2994
3019
  *
2995
- * A session is considered connectable when:
2996
- * - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
2997
- * - Its status is `active`. Pending sessions are skipped here
2998
- * and let the OAuth flow complete separately before we try to reconnect them.
3020
+ * Call this once after creating the client. On long-running servers you
3021
+ * can cache the `MultiSessionClient` instance and call `connect()` on
3022
+ * each request already-connected sessions are skipped internally.
2999
3023
  */
3000
- async getActiveSessions() {
3001
- const sessionList = await sessions.list(this.userId);
3002
- const valid = sessionList.filter(
3024
+ async connect() {
3025
+ const sessions2 = await this.fetchActiveSessions();
3026
+ const activeSessionIds = new Set(sessions2.map((s) => s.sessionId));
3027
+ for (const client of this.clients) {
3028
+ if (!activeSessionIds.has(client.getSessionId())) {
3029
+ this.options.onSessionEvicted?.(client.getSessionId());
3030
+ }
3031
+ }
3032
+ this.clients = this.clients.filter((c) => activeSessionIds.has(c.getSessionId()));
3033
+ await this.connectInBatches(sessions2);
3034
+ }
3035
+ /**
3036
+ * Drops all cached `MCPClient` instances and reconnects fresh from storage.
3037
+ *
3038
+ * Call this when downstream MCP servers have expired their transport sessions
3039
+ * (e.g. after a remote server restart) and subsequent tool calls return
3040
+ * "Session not found. Reconnect without session header." errors.
3041
+ *
3042
+ * OAuth tokens are preserved in the storage backend — no re-authentication
3043
+ * is required. Only the in-memory transport sessions are cleared.
3044
+ */
3045
+ async reconnect() {
3046
+ await this.disconnect();
3047
+ await this.connect();
3048
+ }
3049
+ /**
3050
+ * Returns all currently connected `MCPClient` instances.
3051
+ *
3052
+ * Use this to enumerate available tools across all connected servers,
3053
+ * or to route a tool call to the right client by `serverId`.
3054
+ */
3055
+ getClients() {
3056
+ return this.clients;
3057
+ }
3058
+ /**
3059
+ * Gracefully disconnects all active MCP clients and clears the internal list.
3060
+ *
3061
+ * For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
3062
+ * endpoint per the spec before closing locally. All disconnects run in
3063
+ * parallel so shutdown is not serialised across many sessions.
3064
+ *
3065
+ * Call this during server shutdown or when a user logs out to free up
3066
+ * underlying transport resources (SSE streams, HTTP connections, etc.).
3067
+ */
3068
+ async disconnect() {
3069
+ await Promise.all(this.clients.map((client) => client.disconnect()));
3070
+ this.clients = [];
3071
+ }
3072
+ // -----------------------------------------------------------------------
3073
+ // Internals
3074
+ // -----------------------------------------------------------------------
3075
+ /**
3076
+ * Resolves the list of sessions to connect.
3077
+ *
3078
+ * Uses the custom `sessionProvider` when provided, otherwise falls back
3079
+ * to querying the storage backend via `sessions.list(userId)`.
3080
+ */
3081
+ async fetchActiveSessions() {
3082
+ const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await sessions.list(this.userId);
3083
+ return sessionList.filter(
3003
3084
  (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
3004
3085
  );
3005
- return valid;
3006
3086
  }
3007
3087
  /**
3008
- * Connects to a list of sessions in controlled batches of `CONNECTION_BATCH_SIZE`.
3088
+ * Connects a list of sessions in controlled batches.
3009
3089
  *
3010
- * Batching prevents overwhelming the event loop or external servers when a user
3011
- * has many active MCP sessions (e.g. 20+ servers). Within each batch, sessions
3012
- * are connected concurrently using `Promise.all` for speed.
3090
+ * Batching prevents overwhelming the event loop or external servers when
3091
+ * a user has many active MCP sessions. Within each batch, sessions are
3092
+ * connected concurrently using `Promise.all`.
3013
3093
  */
3014
3094
  async connectInBatches(sessions2) {
3015
3095
  for (let i = 0; i < sessions2.length; i += CONNECTION_BATCH_SIZE) {
@@ -3018,19 +3098,25 @@ var MultiSessionClient = class {
3018
3098
  }
3019
3099
  }
3020
3100
  /**
3021
- * Connects a single session, with built-in deduplication to prevent race conditions.
3101
+ * Connects a single session, with deduplication to prevent race conditions.
3022
3102
  *
3023
- * - If a client for this session already exists and is connected, returns immediately.
3024
- * - If a connection attempt for this session is already in-flight (e.g. from a
3025
- * concurrent call), it joins the existing promise instead of starting a new one.
3026
- * This is the key concurrency lock the `connectionPromises` map acts as a
3027
- * per-session mutex so we never spin up two physical connections for the same session.
3028
- * - On completion (success or failure), the promise is cleaned up from the map.
3103
+ * - If a client for this session already exists and is connected, returns
3104
+ * immediately.
3105
+ * - If the existing client entry is no longer connected (e.g. explicit
3106
+ * disconnect), it is evicted so a fresh transport is created.
3107
+ * - If a connection attempt for this session is already in-flight, the
3108
+ * existing promise is reused as a per-session mutex.
3109
+ * - On completion (success or failure), the promise is cleaned up from
3110
+ * the connectionPromises map.
3029
3111
  */
3030
3112
  async connectSession(session) {
3031
- const existingClient = this.clients.find((c) => c.getSessionId() === session.sessionId);
3032
- if (existingClient?.isConnected()) {
3033
- return;
3113
+ const existing = this.clients.find((c) => c.getSessionId() === session.sessionId);
3114
+ if (existing) {
3115
+ if (existing.isConnected()) {
3116
+ return;
3117
+ }
3118
+ this.options.onSessionEvicted?.(existing.getSessionId());
3119
+ this.clients = this.clients.filter((c) => c !== existing);
3034
3120
  }
3035
3121
  if (this.connectionPromises.has(session.sessionId)) {
3036
3122
  return this.connectionPromises.get(session.sessionId);
@@ -3044,22 +3130,19 @@ var MultiSessionClient = class {
3044
3130
  }
3045
3131
  }
3046
3132
  /**
3047
- * The core connection loop for a single session.
3133
+ * Core connection loop for a single session with retry logic.
3048
3134
  *
3049
- * Attempts to establish a physical MCP connection, retrying up to `maxRetries` times
3050
- * if the connection fails. Each attempt:
3051
- * 1. Creates a fresh `MCPClient` instance from the session data.
3052
- * 2. Races the connect call against a timeout promise — if the server doesn't respond
3053
- * within `timeoutMs`, the attempt is aborted and counted as a failure.
3054
- * 3. On success, replaces any stale client entry for this session in the `clients` array.
3135
+ * 1. Creates a fresh `MCPClient` from the session data.
3136
+ * 2. Races `client.connect()` against a timeout.
3137
+ * 3. On success, replaces any stale entry and fires `onSessionConnected`.
3055
3138
  * 4. On failure, waits `retryDelay` ms before the next attempt.
3056
3139
  *
3057
- * If all attempts are exhausted, logs an error and returns silently (does not throw),
3058
- * so a single bad server doesn't block the rest of the batch from connecting.
3140
+ * If all attempts are exhausted, logs an error and returns silently so
3141
+ * a single bad server doesn't block the rest of the batch.
3059
3142
  */
3060
3143
  async establishConnectionWithRetries(session) {
3061
- const maxRetries = this.options.maxRetries ?? DEFAULT_MAX_RETRIES;
3062
- const retryDelay = this.options.retryDelay ?? DEFAULT_RETRY_DELAY_MS;
3144
+ const maxRetries = this.options.maxRetries;
3145
+ const retryDelay = this.options.retryDelay;
3063
3146
  let lastError;
3064
3147
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
3065
3148
  try {
@@ -3073,10 +3156,13 @@ var MultiSessionClient = class {
3073
3156
  transportType: session.transportType,
3074
3157
  headers: session.headers
3075
3158
  });
3076
- const timeoutMs = this.options.timeout ?? DEFAULT_TIMEOUT_MS;
3159
+ const timeoutMs = this.options.timeout;
3077
3160
  let timeoutTimer;
3078
3161
  const timeoutPromise = new Promise((_, reject) => {
3079
- timeoutTimer = setTimeout(() => reject(new Error(`Connection timed out after ${timeoutMs}ms`)), timeoutMs);
3162
+ timeoutTimer = setTimeout(
3163
+ () => reject(new Error(`Connection timed out after ${timeoutMs}ms`)),
3164
+ timeoutMs
3165
+ );
3080
3166
  });
3081
3167
  try {
3082
3168
  await Promise.race([client.connect(), timeoutPromise]);
@@ -3085,6 +3171,7 @@ var MultiSessionClient = class {
3085
3171
  }
3086
3172
  this.clients = this.clients.filter((c) => c.getSessionId() !== session.sessionId);
3087
3173
  this.clients.push(client);
3174
+ this.options.onSessionConnected?.(session.sessionId, client);
3088
3175
  return;
3089
3176
  } catch (error) {
3090
3177
  lastError = error;
@@ -3093,38 +3180,11 @@ var MultiSessionClient = class {
3093
3180
  }
3094
3181
  }
3095
3182
  }
3096
- console.error(`[MultiSessionClient] Failed to connect to session ${session.sessionId} after ${maxRetries + 1} attempts:`, lastError);
3097
- }
3098
- /**
3099
- * The main entry point. Fetches all active sessions for this userId from
3100
- * storage and establishes connections to all of them in batches.
3101
- *
3102
- * Call this once after creating the client. On traditional servers, you can
3103
- * cache the `MultiSessionClient` instance after calling `connect()` to avoid
3104
- * re-fetching and re-connecting on every request.
3105
- */
3106
- async connect() {
3107
- const sessions2 = await this.getActiveSessions();
3108
- await this.connectInBatches(sessions2);
3109
- }
3110
- /**
3111
- * Returns all currently connected `MCPClient` instances.
3112
- *
3113
- * Use this to enumerate available tools across all connected servers,
3114
- * or to route a tool call to the right client by `serverId`.
3115
- */
3116
- getClients() {
3117
- return this.clients;
3118
- }
3119
- /**
3120
- * Gracefully disconnects all active MCP clients and clears the internal client list.
3121
- *
3122
- * Call this during server shutdown or when a user logs out to free up
3123
- * underlying transport resources (SSE streams, HTTP connections, etc.).
3124
- */
3125
- disconnect() {
3126
- this.clients.forEach((client) => client.disconnect());
3127
- this.clients = [];
3183
+ this.options.onSessionFailed?.(session.sessionId, lastError);
3184
+ console.error(
3185
+ `[MultiSessionClient] Failed to connect to session ${session.sessionId} after ${maxRetries + 1} attempts:`,
3186
+ lastError
3187
+ );
3128
3188
  }
3129
3189
  };
3130
3190
 
@@ -3357,7 +3417,6 @@ var SSEConnectionManager = class {
3357
3417
  const client = this.clients.get(sessionId);
3358
3418
  if (client) {
3359
3419
  await client.clearSession();
3360
- client.disconnect();
3361
3420
  this.clients.delete(sessionId);
3362
3421
  } else {
3363
3422
  await sessions.delete(this.userId, sessionId);
@@ -3566,14 +3625,14 @@ var SSEConnectionManager = class {
3566
3625
  /**
3567
3626
  * Cleanup and close all connections
3568
3627
  */
3569
- dispose() {
3628
+ async dispose() {
3570
3629
  this.isActive = false;
3571
3630
  if (this.heartbeatTimer) {
3572
3631
  clearInterval(this.heartbeatTimer);
3573
3632
  }
3574
- for (const client of this.clients.values()) {
3575
- client.disconnect();
3576
- }
3633
+ await Promise.all(
3634
+ Array.from(this.clients.values()).map((client) => client.disconnect())
3635
+ );
3577
3636
  this.clients.clear();
3578
3637
  }
3579
3638
  };