@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.mjs CHANGED
@@ -2290,7 +2290,7 @@ var MCPClient = class {
2290
2290
  * Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
2291
2291
  * This wrapper enforces a timeout and properly uses AbortController to unblock the request.
2292
2292
  */
2293
- fetch: (url, init) => {
2293
+ fetch: async (url, init) => {
2294
2294
  const timeout = 3e4;
2295
2295
  const controller = new AbortController();
2296
2296
  const timeoutId = setTimeout(() => controller.abort(), timeout);
@@ -2298,7 +2298,17 @@ var MCPClient = class {
2298
2298
  // @ts-ignore: AbortSignal.any is available in Node 20+
2299
2299
  AbortSignal.any ? AbortSignal.any([init.signal, controller.signal]) : controller.signal
2300
2300
  ) : controller.signal;
2301
- return fetch(url, { ...init, signal }).finally(() => clearTimeout(timeoutId));
2301
+ try {
2302
+ const response = await fetch(url, { ...init, signal });
2303
+ const hasSessionHeader = init?.headers && new Headers(init.headers).has("mcp-session-id");
2304
+ if (response.status === 404 && hasSessionHeader) {
2305
+ this.client = null;
2306
+ throw new Error("MCP_SESSION_EXPIRED: Downstream session was not found on the server.");
2307
+ }
2308
+ return response;
2309
+ } finally {
2310
+ clearTimeout(timeoutId);
2311
+ }
2302
2312
  }
2303
2313
  };
2304
2314
  if (type === "sse") {
@@ -2490,6 +2500,14 @@ var MCPClient = class {
2490
2500
  * @throws {Error} When connection fails for other reasons
2491
2501
  */
2492
2502
  async connect() {
2503
+ if (this.client?.transport) {
2504
+ this.transport = null;
2505
+ try {
2506
+ await this.client.close();
2507
+ } catch {
2508
+ }
2509
+ this.client = null;
2510
+ }
2493
2511
  await this.initialize();
2494
2512
  if (!this.client || !this.oauthProvider) {
2495
2513
  const error = "Client or OAuth provider not initialized";
@@ -2868,7 +2886,7 @@ var MCPClient = class {
2868
2886
  await this.oauthProvider.invalidateCredentials("all");
2869
2887
  }
2870
2888
  await sessions.delete(this.userId, this.sessionId);
2871
- this.disconnect();
2889
+ await this.disconnect();
2872
2890
  }
2873
2891
  /**
2874
2892
  * Checks if the client is currently connected to an MCP server
@@ -2878,10 +2896,21 @@ var MCPClient = class {
2878
2896
  return this.client !== null;
2879
2897
  }
2880
2898
  /**
2881
- * Disconnects from the MCP server and cleans up resources
2882
- * Does not remove session from Redis - use clearSession() for that
2899
+ * Disconnects from the MCP server and cleans up resources.
2900
+ * Does not remove session from Redis use clearSession() for that.
2901
+ *
2902
+ * For Streamable HTTP sessions, sends an HTTP DELETE to the MCP endpoint
2903
+ * before closing, as recommended by the MCP Streamable HTTP spec
2904
+ * (section "Session Management", rule 5). This is best-effort — errors
2905
+ * (e.g. server already restarted, 404/405 responses) are silently ignored.
2883
2906
  */
2884
- disconnect(reason) {
2907
+ async disconnect() {
2908
+ if (this.transport instanceof StreamableHTTPClientTransport) {
2909
+ try {
2910
+ await this.transport.terminateSession();
2911
+ } catch {
2912
+ }
2913
+ }
2885
2914
  if (this.client) {
2886
2915
  this.client.close();
2887
2916
  }
@@ -2893,7 +2922,6 @@ var MCPClient = class {
2893
2922
  type: "disconnected",
2894
2923
  sessionId: this.sessionId,
2895
2924
  serverId: this.serverId,
2896
- reason,
2897
2925
  timestamp: Date.now()
2898
2926
  });
2899
2927
  this._onObservabilityEvent.fire({
@@ -2902,9 +2930,7 @@ var MCPClient = class {
2902
2930
  message: `Disconnected from ${this.serverId}`,
2903
2931
  sessionId: this.sessionId,
2904
2932
  serverId: this.serverId,
2905
- payload: {
2906
- reason: reason || "unknown"
2907
- },
2933
+ payload: {},
2908
2934
  timestamp: Date.now(),
2909
2935
  id: nanoid()
2910
2936
  });
@@ -3022,6 +3048,9 @@ var MultiSessionClient = class {
3022
3048
  * Connects a single session, with built-in deduplication to prevent race conditions.
3023
3049
  *
3024
3050
  * - If a client for this session already exists and is connected, returns immediately.
3051
+ * - If the existing client entry is no longer connected (e.g. it was explicitly
3052
+ * disconnected), it is evicted so that `establishConnectionWithRetries` creates a
3053
+ * fresh transport — preventing "Client already connected" errors from the SDK.
3025
3054
  * - If a connection attempt for this session is already in-flight (e.g. from a
3026
3055
  * concurrent call), it joins the existing promise instead of starting a new one.
3027
3056
  * This is the key concurrency lock — the `connectionPromises` map acts as a
@@ -3029,9 +3058,12 @@ var MultiSessionClient = class {
3029
3058
  * - On completion (success or failure), the promise is cleaned up from the map.
3030
3059
  */
3031
3060
  async connectSession(session) {
3032
- const existingClient = this.clients.find((c) => c.getSessionId() === session.sessionId);
3033
- if (existingClient?.isConnected()) {
3034
- return;
3061
+ const existing = this.clients.find((c) => c.getSessionId() === session.sessionId);
3062
+ if (existing) {
3063
+ if (existing.isConnected()) {
3064
+ return;
3065
+ }
3066
+ this.clients = this.clients.filter((c) => c !== existing);
3035
3067
  }
3036
3068
  if (this.connectionPromises.has(session.sessionId)) {
3037
3069
  return this.connectionPromises.get(session.sessionId);
@@ -3106,8 +3138,24 @@ var MultiSessionClient = class {
3106
3138
  */
3107
3139
  async connect() {
3108
3140
  const sessions2 = await this.getActiveSessions();
3141
+ const activeSessionIds = new Set(sessions2.map((s) => s.sessionId));
3142
+ this.clients = this.clients.filter((c) => activeSessionIds.has(c.getSessionId()));
3109
3143
  await this.connectInBatches(sessions2);
3110
3144
  }
3145
+ /**
3146
+ * Drops all cached `MCPClient` instances and reconnects fresh from storage.
3147
+ *
3148
+ * Call this when downstream MCP servers have expired their transport sessions
3149
+ * (e.g. after a remote server restart) and subsequent tool calls return
3150
+ * "Session not found. Reconnect without session header." errors.
3151
+ *
3152
+ * OAuth tokens are preserved in the storage backend — no re-authentication
3153
+ * is required. Only the in-memory transport sessions are cleared.
3154
+ */
3155
+ async reconnect() {
3156
+ await this.disconnect();
3157
+ await this.connect();
3158
+ }
3111
3159
  /**
3112
3160
  * Returns all currently connected `MCPClient` instances.
3113
3161
  *
@@ -3120,11 +3168,15 @@ var MultiSessionClient = class {
3120
3168
  /**
3121
3169
  * Gracefully disconnects all active MCP clients and clears the internal client list.
3122
3170
  *
3171
+ * For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
3172
+ * endpoint per the spec before closing locally. All disconnects run in
3173
+ * parallel so shutdown is not serialised across many sessions.
3174
+ *
3123
3175
  * Call this during server shutdown or when a user logs out to free up
3124
3176
  * underlying transport resources (SSE streams, HTTP connections, etc.).
3125
3177
  */
3126
- disconnect() {
3127
- this.clients.forEach((client) => client.disconnect());
3178
+ async disconnect() {
3179
+ await Promise.all(this.clients.map((client) => client.disconnect()));
3128
3180
  this.clients = [];
3129
3181
  }
3130
3182
  };
@@ -3354,7 +3406,6 @@ var SSEConnectionManager = class {
3354
3406
  const client = this.clients.get(sessionId);
3355
3407
  if (client) {
3356
3408
  await client.clearSession();
3357
- client.disconnect();
3358
3409
  this.clients.delete(sessionId);
3359
3410
  } else {
3360
3411
  await sessions.delete(this.userId, sessionId);
@@ -3563,14 +3614,14 @@ var SSEConnectionManager = class {
3563
3614
  /**
3564
3615
  * Cleanup and close all connections
3565
3616
  */
3566
- dispose() {
3617
+ async dispose() {
3567
3618
  this.isActive = false;
3568
3619
  if (this.heartbeatTimer) {
3569
3620
  clearInterval(this.heartbeatTimer);
3570
3621
  }
3571
- for (const client of this.clients.values()) {
3572
- client.disconnect();
3573
- }
3622
+ await Promise.all(
3623
+ Array.from(this.clients.values()).map((client) => client.disconnect())
3624
+ );
3574
3625
  this.clients.clear();
3575
3626
  }
3576
3627
  };