@mcp-ts/sdk 2.4.0 → 2.4.1

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 (48) hide show
  1. package/README.md +58 -19
  2. package/dist/adapters/agui-adapter.d.mts +2 -2
  3. package/dist/adapters/agui-adapter.d.ts +2 -2
  4. package/dist/adapters/agui-middleware.d.mts +2 -2
  5. package/dist/adapters/agui-middleware.d.ts +2 -2
  6. package/dist/adapters/ai-adapter.d.mts +2 -2
  7. package/dist/adapters/ai-adapter.d.ts +2 -2
  8. package/dist/adapters/langchain-adapter.d.mts +2 -2
  9. package/dist/adapters/langchain-adapter.d.ts +2 -2
  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 -2
  13. package/dist/client/index.d.ts +2 -2
  14. package/dist/client/react.d.mts +4 -4
  15. package/dist/client/react.d.ts +4 -4
  16. package/dist/client/vue.d.mts +4 -4
  17. package/dist/client/vue.d.ts +4 -4
  18. package/dist/{events-CK3N--3g.d.mts → events-C4m7tK1U.d.mts} +0 -1
  19. package/dist/{events-CK3N--3g.d.ts → events-C4m7tK1U.d.ts} +0 -1
  20. package/dist/{index-CtXvKl8N.d.ts → index-Ch7ouNSa.d.ts} +1 -1
  21. package/dist/{index-ByIjEReo.d.mts → index-L5XoXgsb.d.mts} +1 -1
  22. package/dist/index.d.mts +3 -3
  23. package/dist/index.d.ts +3 -3
  24. package/dist/index.js +71 -20
  25. package/dist/index.js.map +1 -1
  26. package/dist/index.mjs +71 -20
  27. package/dist/index.mjs.map +1 -1
  28. package/dist/{multi-session-client-CnvZEGPY.d.ts → multi-session-client-Bwm-efqg.d.ts} +28 -5
  29. package/dist/{multi-session-client-CIMUGF8S.d.mts → multi-session-client-k-9RvWvi.d.mts} +28 -5
  30. package/dist/server/index.d.mts +4 -4
  31. package/dist/server/index.d.ts +4 -4
  32. package/dist/server/index.js +71 -20
  33. package/dist/server/index.js.map +1 -1
  34. package/dist/server/index.mjs +71 -20
  35. package/dist/server/index.mjs.map +1 -1
  36. package/dist/shared/index.d.mts +1 -1
  37. package/dist/shared/index.d.ts +1 -1
  38. package/dist/shared/index.js.map +1 -1
  39. package/dist/shared/index.mjs.map +1 -1
  40. package/migrations/v2.3.4/neon/20260513010000_install_mcp_sessions.sql +69 -0
  41. package/migrations/v2.3.4/neon/20260513020000_add_session_cleanup_cron.sql +35 -0
  42. package/migrations/v2.3.4/supabase/20260330195700_install_mcp_sessions.sql +82 -0
  43. package/migrations/v2.3.4/supabase/20260421010000_add_session_cleanup_cron.sql +32 -0
  44. package/package.json +1 -1
  45. package/src/server/handlers/sse-handler.ts +7 -5
  46. package/src/server/mcp/multi-session-client.ts +46 -5
  47. package/src/server/mcp/oauth-client.ts +60 -10
  48. package/src/shared/events.ts +0 -1
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
  });
@@ -3096,6 +3122,9 @@ var MultiSessionClient = class {
3096
3122
  * Connects a single session, with built-in deduplication to prevent race conditions.
3097
3123
  *
3098
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.
3099
3128
  * - If a connection attempt for this session is already in-flight (e.g. from a
3100
3129
  * concurrent call), it joins the existing promise instead of starting a new one.
3101
3130
  * This is the key concurrency lock — the `connectionPromises` map acts as a
@@ -3103,9 +3132,12 @@ var MultiSessionClient = class {
3103
3132
  * - On completion (success or failure), the promise is cleaned up from the map.
3104
3133
  */
3105
3134
  async connectSession(session) {
3106
- const existingClient = this.clients.find((c) => c.getSessionId() === session.sessionId);
3107
- if (existingClient?.isConnected()) {
3108
- return;
3135
+ const existing = this.clients.find((c) => c.getSessionId() === session.sessionId);
3136
+ if (existing) {
3137
+ if (existing.isConnected()) {
3138
+ return;
3139
+ }
3140
+ this.clients = this.clients.filter((c) => c !== existing);
3109
3141
  }
3110
3142
  if (this.connectionPromises.has(session.sessionId)) {
3111
3143
  return this.connectionPromises.get(session.sessionId);
@@ -3180,8 +3212,24 @@ var MultiSessionClient = class {
3180
3212
  */
3181
3213
  async connect() {
3182
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()));
3183
3217
  await this.connectInBatches(sessions2);
3184
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
+ }
3185
3233
  /**
3186
3234
  * Returns all currently connected `MCPClient` instances.
3187
3235
  *
@@ -3194,11 +3242,15 @@ var MultiSessionClient = class {
3194
3242
  /**
3195
3243
  * Gracefully disconnects all active MCP clients and clears the internal client list.
3196
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
+ *
3197
3249
  * Call this during server shutdown or when a user logs out to free up
3198
3250
  * underlying transport resources (SSE streams, HTTP connections, etc.).
3199
3251
  */
3200
- disconnect() {
3201
- this.clients.forEach((client) => client.disconnect());
3252
+ async disconnect() {
3253
+ await Promise.all(this.clients.map((client) => client.disconnect()));
3202
3254
  this.clients = [];
3203
3255
  }
3204
3256
  };
@@ -3432,7 +3484,6 @@ var SSEConnectionManager = class {
3432
3484
  const client = this.clients.get(sessionId);
3433
3485
  if (client) {
3434
3486
  await client.clearSession();
3435
- client.disconnect();
3436
3487
  this.clients.delete(sessionId);
3437
3488
  } else {
3438
3489
  await sessions.delete(this.userId, sessionId);
@@ -3641,14 +3692,14 @@ var SSEConnectionManager = class {
3641
3692
  /**
3642
3693
  * Cleanup and close all connections
3643
3694
  */
3644
- dispose() {
3695
+ async dispose() {
3645
3696
  this.isActive = false;
3646
3697
  if (this.heartbeatTimer) {
3647
3698
  clearInterval(this.heartbeatTimer);
3648
3699
  }
3649
- for (const client of this.clients.values()) {
3650
- client.disconnect();
3651
- }
3700
+ await Promise.all(
3701
+ Array.from(this.clients.values()).map((client) => client.disconnect())
3702
+ );
3652
3703
  this.clients.clear();
3653
3704
  }
3654
3705
  };