@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
@@ -1,54 +1,90 @@
1
-
2
-
3
1
  import { MCPClient } from './oauth-client.js';
4
2
  import { sessions, type Session } from '../storage/index.js';
3
+ import type { ToolClientProvider } from '../../shared/types.js';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Constants
7
+ // ---------------------------------------------------------------------------
5
8
 
6
9
  const DEFAULT_TIMEOUT_MS = 15000;
7
10
  const DEFAULT_MAX_RETRIES = 2;
8
11
  const DEFAULT_RETRY_DELAY_MS = 1000;
9
12
  const CONNECTION_BATCH_SIZE = 5;
10
13
 
11
- /**
12
- * Manages multiple MCP connections for a single user.
13
- * Allows aggregating tools from all connected servers.
14
- */
14
+ // ---------------------------------------------------------------------------
15
+ // Types
16
+ // ---------------------------------------------------------------------------
17
+
15
18
  export interface MultiSessionOptions {
16
19
  /**
17
- * Connection timeout in milliseconds
20
+ * Connection timeout in milliseconds.
18
21
  * @default 15000
19
22
  */
20
23
  timeout?: number;
24
+
21
25
  /**
22
- * Maximum number of retry attempts
26
+ * Maximum number of retry attempts per session.
23
27
  * @default 2
24
28
  */
25
29
  maxRetries?: number;
30
+
26
31
  /**
27
- * Delay between retries in milliseconds
32
+ * Delay between retry attempts in milliseconds.
28
33
  * @default 1000
29
34
  */
30
35
  retryDelay?: number;
36
+
37
+ /**
38
+ * Custom session provider. When provided, `connect()` calls this
39
+ * instead of querying the storage backend. Useful when sessions are
40
+ * synced externally (e.g. via Supabase Realtime, WebSocket, or an
41
+ * in-memory cache).
42
+ *
43
+ * Default: reads from the storage backend via `sessions.list(userId)`.
44
+ */
45
+ sessionProvider?: () => Promise<Session[]>;
46
+
47
+ /**
48
+ * Called after a session is successfully connected.
49
+ */
50
+ onSessionConnected?: (sessionId: string, client: MCPClient) => void;
51
+
52
+ /**
53
+ * Called when a session is evicted from the in-memory client list
54
+ * because it no longer exists in the active sessions list.
55
+ */
56
+ onSessionEvicted?: (sessionId: string) => void;
57
+
58
+ /**
59
+ * Called when all retry attempts for a session have been exhausted.
60
+ */
61
+ onSessionFailed?: (sessionId: string, error: unknown) => void;
31
62
  }
32
63
 
64
+ // ---------------------------------------------------------------------------
65
+ // MultiSessionClient
66
+ // ---------------------------------------------------------------------------
67
+
33
68
  /**
34
69
  * Manages multiple MCP client connections for a single user.
35
70
  *
36
- * On a traditional long-running server, you can cache this instance per user
37
- * so the connections stay alive between requests. On serverless, a new instance
38
- * will be created per invocation, but the underlying session data is always
39
- * read from the storage backend so nothing is lost between calls.
71
+ * Cached instances stay alive between requests on long-running servers.
72
+ * On serverless, the underlying session data persists in storage the
73
+ * client rehydrates from `sessionProvider` (or the default storage backend)
74
+ * each time `connect()` is called.
75
+ *
76
+ * @implements {ToolClientProvider}
40
77
  */
41
- export class MultiSessionClient {
78
+ export class MultiSessionClient implements ToolClientProvider {
42
79
  private clients: MCPClient[] = [];
80
+ private connectionPromises = new Map<string, Promise<void>>();
43
81
  private userId: string;
44
- private options: MultiSessionOptions;
82
+ private options: Required<Pick<MultiSessionOptions, 'timeout' | 'maxRetries' | 'retryDelay'>> &
83
+ Pick<MultiSessionOptions, 'sessionProvider' | 'onSessionConnected' | 'onSessionEvicted' | 'onSessionFailed'>;
45
84
 
46
85
  /**
47
- * Creates a new MultiSessionClient for the given user userId.
48
- *
49
- * @param userId - A unique string identifying the user (e.g. user ID or email).
50
- * @param options - Optional tuning for connection timeout, retry count, and retry delay.
51
- * Falls back to sensible defaults if not provided.
86
+ * @param userId - Unique identifier for the user (e.g. user ID or email).
87
+ * @param options - Optional tuning and lifecycle hooks.
52
88
  */
53
89
  constructor(userId: string, options: MultiSessionOptions = {}) {
54
90
  this.userId = userId;
@@ -56,36 +92,110 @@ export class MultiSessionClient {
56
92
  timeout: DEFAULT_TIMEOUT_MS,
57
93
  maxRetries: DEFAULT_MAX_RETRIES,
58
94
  retryDelay: DEFAULT_RETRY_DELAY_MS,
59
- ...options
95
+ ...options,
60
96
  };
61
97
  }
62
98
 
99
+ // -----------------------------------------------------------------------
100
+ // Public API
101
+ // -----------------------------------------------------------------------
102
+
103
+ /**
104
+ * Fetches active sessions and establishes connections to all of them.
105
+ *
106
+ * Call this once after creating the client. On long-running servers you
107
+ * can cache the `MultiSessionClient` instance and call `connect()` on
108
+ * each request — already-connected sessions are skipped internally.
109
+ */
110
+ async connect(): Promise<void> {
111
+ const sessions = await this.fetchActiveSessions();
112
+ const activeSessionIds = new Set(sessions.map(s => s.sessionId));
113
+
114
+ // Evict clients whose sessions no longer appear in the active list.
115
+ // Without this, a stale MCPClient with a live SDK transport would
116
+ // stay in the clients array after its session was deleted externally
117
+ // (e.g. via the SSE handler's clearSession()), causing OAuth refreshes
118
+ // to fail with a FK violation when they try to patchCredentials()
119
+ // against the now-deleted session.
120
+ for (const client of this.clients) {
121
+ if (!activeSessionIds.has(client.getSessionId())) {
122
+ this.options.onSessionEvicted?.(client.getSessionId());
123
+ }
124
+ }
125
+ this.clients = this.clients.filter(c => activeSessionIds.has(c.getSessionId()));
126
+
127
+ await this.connectInBatches(sessions);
128
+ }
129
+
130
+ /**
131
+ * Drops all cached `MCPClient` instances and reconnects fresh from storage.
132
+ *
133
+ * Call this when downstream MCP servers have expired their transport sessions
134
+ * (e.g. after a remote server restart) and subsequent tool calls return
135
+ * "Session not found. Reconnect without session header." errors.
136
+ *
137
+ * OAuth tokens are preserved in the storage backend — no re-authentication
138
+ * is required. Only the in-memory transport sessions are cleared.
139
+ */
140
+ async reconnect(): Promise<void> {
141
+ await this.disconnect();
142
+ await this.connect();
143
+ }
144
+
145
+ /**
146
+ * Returns all currently connected `MCPClient` instances.
147
+ *
148
+ * Use this to enumerate available tools across all connected servers,
149
+ * or to route a tool call to the right client by `serverId`.
150
+ */
151
+ getClients(): MCPClient[] {
152
+ return this.clients;
153
+ }
154
+
63
155
  /**
64
- * Fetches all sessions for this userId from storage and returns only the
65
- * ones that are ready to connect.
156
+ * Gracefully disconnects all active MCP clients and clears the internal list.
66
157
  *
67
- * A session is considered connectable when:
68
- * - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
69
- * - Its status is `active`. Pending sessions are skipped here
70
- * and let the OAuth flow complete separately before we try to reconnect them.
71
- */
72
- private async getActiveSessions(): Promise<Session[]> {
73
- const sessionList = await sessions.list(this.userId);
74
- const valid = sessionList.filter(s =>
158
+ * For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
159
+ * endpoint per the spec before closing locally. All disconnects run in
160
+ * parallel so shutdown is not serialised across many sessions.
161
+ *
162
+ * Call this during server shutdown or when a user logs out to free up
163
+ * underlying transport resources (SSE streams, HTTP connections, etc.).
164
+ */
165
+ async disconnect(): Promise<void> {
166
+ await Promise.all(this.clients.map((client) => client.disconnect()));
167
+ this.clients = [];
168
+ }
169
+
170
+ // -----------------------------------------------------------------------
171
+ // Internals
172
+ // -----------------------------------------------------------------------
173
+
174
+ /**
175
+ * Resolves the list of sessions to connect.
176
+ *
177
+ * Uses the custom `sessionProvider` when provided, otherwise falls back
178
+ * to querying the storage backend via `sessions.list(userId)`.
179
+ */
180
+ private async fetchActiveSessions(): Promise<Session[]> {
181
+ const sessionList = this.options.sessionProvider
182
+ ? await this.options.sessionProvider()
183
+ : await sessions.list(this.userId);
184
+
185
+ return sessionList.filter(s =>
75
186
  s.serverId &&
76
187
  s.serverUrl &&
77
188
  s.callbackUrl &&
78
189
  s.status === 'active'
79
190
  );
80
- return valid;
81
191
  }
82
192
 
83
193
  /**
84
- * Connects to a list of sessions in controlled batches of `CONNECTION_BATCH_SIZE`.
194
+ * Connects a list of sessions in controlled batches.
85
195
  *
86
- * Batching prevents overwhelming the event loop or external servers when a user
87
- * has many active MCP sessions (e.g. 20+ servers). Within each batch, sessions
88
- * are connected concurrently using `Promise.all` for speed.
196
+ * Batching prevents overwhelming the event loop or external servers when
197
+ * a user has many active MCP sessions. Within each batch, sessions are
198
+ * connected concurrently using `Promise.all`.
89
199
  */
90
200
  private async connectInBatches(sessions: Session[]): Promise<void> {
91
201
  for (let i = 0; i < sessions.length; i += CONNECTION_BATCH_SIZE) {
@@ -94,25 +204,30 @@ export class MultiSessionClient {
94
204
  }
95
205
  }
96
206
 
97
- private connectionPromises = new Map<string, Promise<void>>();
98
-
99
207
  /**
100
- * Connects a single session, with built-in deduplication to prevent race conditions.
208
+ * Connects a single session, with deduplication to prevent race conditions.
101
209
  *
102
- * - If a client for this session already exists and is connected, returns immediately.
103
- * - If a connection attempt for this session is already in-flight (e.g. from a
104
- * concurrent call), it joins the existing promise instead of starting a new one.
105
- * This is the key concurrency lock the `connectionPromises` map acts as a
106
- * per-session mutex so we never spin up two physical connections for the same session.
107
- * - On completion (success or failure), the promise is cleaned up from the map.
210
+ * - If a client for this session already exists and is connected, returns
211
+ * immediately.
212
+ * - If the existing client entry is no longer connected (e.g. explicit
213
+ * disconnect), it is evicted so a fresh transport is created.
214
+ * - If a connection attempt for this session is already in-flight, the
215
+ * existing promise is reused as a per-session mutex.
216
+ * - On completion (success or failure), the promise is cleaned up from
217
+ * the connectionPromises map.
108
218
  */
109
219
  private async connectSession(session: Session): Promise<void> {
110
- const existingClient = this.clients.find(c => c.getSessionId() === session.sessionId);
111
- if (existingClient?.isConnected()) {
112
- return;
220
+ const existing = this.clients.find(c => c.getSessionId() === session.sessionId);
221
+
222
+ if (existing) {
223
+ if (existing.isConnected()) {
224
+ return;
225
+ }
226
+
227
+ this.options.onSessionEvicted?.(existing.getSessionId());
228
+ this.clients = this.clients.filter(c => c !== existing);
113
229
  }
114
230
 
115
- // Avoid concurrent connection attempts for the same session
116
231
  if (this.connectionPromises.has(session.sessionId)) {
117
232
  return this.connectionPromises.get(session.sessionId)!;
118
233
  }
@@ -129,22 +244,19 @@ export class MultiSessionClient {
129
244
  }
130
245
 
131
246
  /**
132
- * The core connection loop for a single session.
247
+ * Core connection loop for a single session with retry logic.
133
248
  *
134
- * Attempts to establish a physical MCP connection, retrying up to `maxRetries` times
135
- * if the connection fails. Each attempt:
136
- * 1. Creates a fresh `MCPClient` instance from the session data.
137
- * 2. Races the connect call against a timeout promise — if the server doesn't respond
138
- * within `timeoutMs`, the attempt is aborted and counted as a failure.
139
- * 3. On success, replaces any stale client entry for this session in the `clients` array.
249
+ * 1. Creates a fresh `MCPClient` from the session data.
250
+ * 2. Races `client.connect()` against a timeout.
251
+ * 3. On success, replaces any stale entry and fires `onSessionConnected`.
140
252
  * 4. On failure, waits `retryDelay` ms before the next attempt.
141
253
  *
142
- * If all attempts are exhausted, logs an error and returns silently (does not throw),
143
- * so a single bad server doesn't block the rest of the batch from connecting.
254
+ * If all attempts are exhausted, logs an error and returns silently so
255
+ * a single bad server doesn't block the rest of the batch.
144
256
  */
145
257
  private async establishConnectionWithRetries(session: Session): Promise<void> {
146
- const maxRetries = this.options.maxRetries ?? DEFAULT_MAX_RETRIES;
147
- const retryDelay = this.options.retryDelay ?? DEFAULT_RETRY_DELAY_MS;
258
+ const maxRetries = this.options.maxRetries;
259
+ const retryDelay = this.options.retryDelay;
148
260
  let lastError: unknown;
149
261
 
150
262
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
@@ -160,10 +272,13 @@ export class MultiSessionClient {
160
272
  headers: session.headers,
161
273
  });
162
274
 
163
- const timeoutMs = this.options.timeout ?? DEFAULT_TIMEOUT_MS;
275
+ const timeoutMs = this.options.timeout;
164
276
  let timeoutTimer: ReturnType<typeof setTimeout>;
165
277
  const timeoutPromise = new Promise<never>((_, reject) => {
166
- timeoutTimer = setTimeout(() => reject(new Error(`Connection timed out after ${timeoutMs}ms`)), timeoutMs);
278
+ timeoutTimer = setTimeout(
279
+ () => reject(new Error(`Connection timed out after ${timeoutMs}ms`)),
280
+ timeoutMs,
281
+ );
167
282
  });
168
283
 
169
284
  try {
@@ -172,10 +287,11 @@ export class MultiSessionClient {
172
287
  clearTimeout(timeoutTimer!);
173
288
  }
174
289
 
175
- // Always replace the disconnected client entry
176
290
  this.clients = this.clients.filter(c => c.getSessionId() !== session.sessionId);
177
291
  this.clients.push(client);
178
- return; // successfully connected
292
+ this.options.onSessionConnected?.(session.sessionId, client);
293
+ return;
294
+
179
295
  } catch (error) {
180
296
  lastError = error;
181
297
  if (attempt < maxRetries) {
@@ -184,41 +300,11 @@ export class MultiSessionClient {
184
300
  }
185
301
  }
186
302
 
187
- console.error(`[MultiSessionClient] Failed to connect to session ${session.sessionId} after ${maxRetries + 1} attempts:`, lastError);
188
- }
189
-
190
- /**
191
- * The main entry point. Fetches all active sessions for this userId from
192
- * storage and establishes connections to all of them in batches.
193
- *
194
- * Call this once after creating the client. On traditional servers, you can
195
- * cache the `MultiSessionClient` instance after calling `connect()` to avoid
196
- * re-fetching and re-connecting on every request.
197
- */
198
- async connect(): Promise<void> {
199
- const sessions = await this.getActiveSessions();
200
- await this.connectInBatches(sessions);
201
- }
202
-
203
- /**
204
- * Returns all currently connected `MCPClient` instances.
205
- *
206
- * Use this to enumerate available tools across all connected servers,
207
- * or to route a tool call to the right client by `serverId`.
208
- */
209
- getClients(): MCPClient[] {
210
- return this.clients;
211
- }
212
-
213
- /**
214
- * Gracefully disconnects all active MCP clients and clears the internal client list.
215
- *
216
- * Call this during server shutdown or when a user logs out to free up
217
- * underlying transport resources (SSE streams, HTTP connections, etc.).
218
- */
219
- disconnect(): void {
220
- this.clients.forEach((client) => client.disconnect());
221
- this.clients = [];
303
+ this.options.onSessionFailed?.(session.sessionId, lastError);
304
+ console.error(
305
+ `[MultiSessionClient] Failed to connect to session ${session.sessionId} ` +
306
+ `after ${maxRetries + 1} attempts:`,
307
+ lastError,
308
+ );
222
309
  }
223
310
  }
224
-
@@ -242,7 +242,7 @@ export class MCPClient {
242
242
  * Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
243
243
  * This wrapper enforces a timeout and properly uses AbortController to unblock the request.
244
244
  */
245
- fetch: (url: RequestInfo | URL, init?: RequestInit) => {
245
+ fetch: async (url: RequestInfo | URL, init?: RequestInit) => {
246
246
  const timeout = 30000;
247
247
  const controller = new AbortController();
248
248
  const timeoutId = setTimeout(() => controller.abort(), timeout);
@@ -251,7 +251,20 @@ export class MCPClient {
251
251
  (AbortSignal.any ? AbortSignal.any([init.signal, controller.signal]) : controller.signal) :
252
252
  controller.signal;
253
253
 
254
- return fetch(url, { ...init, signal }).finally(() => clearTimeout(timeoutId));
254
+ try {
255
+ const response = await fetch(url, { ...init, signal });
256
+
257
+ const hasSessionHeader = init?.headers && new Headers(init.headers as HeadersInit).has('mcp-session-id');
258
+
259
+ if (response.status === 404 && hasSessionHeader) {
260
+ this.client = null;
261
+ throw new Error("MCP_SESSION_EXPIRED: Downstream session was not found on the server.");
262
+ }
263
+
264
+ return response;
265
+ } finally {
266
+ clearTimeout(timeoutId);
267
+ }
255
268
  }
256
269
  };
257
270
 
@@ -497,6 +510,27 @@ export class MCPClient {
497
510
  * @throws {Error} When connection fails for other reasons
498
511
  */
499
512
  async connect(): Promise<void> {
513
+ // Re-entry guard: if a previous SDK Client is still attached to a transport
514
+ // (e.g. a stale session from a prior connect() call on the same MCPClient
515
+ // instance), close and detach it before proceeding. The MCP SDK client will
516
+ // throw if asked to connect while a transport is already attached, and the
517
+ // stale transport would send an expired mcp-session-id to the remote server
518
+ // causing "Session not found. Reconnect without session header." errors.
519
+ //
520
+ // We also null out this.client so that initialize() creates a fresh Client
521
+ // instance with a clean transport slot, rather than reusing the old one.
522
+ // The oauthProvider is intentionally preserved — OAuth tokens remain valid
523
+ // across reconnects; only the transport session needs to be renegotiated.
524
+ if (this.client?.transport) {
525
+ this.transport = null;
526
+ try {
527
+ await this.client.close();
528
+ } catch {
529
+ // Closing a transport that may have already failed is best-effort.
530
+ }
531
+ this.client = null;
532
+ }
533
+
500
534
  await this.initialize();
501
535
 
502
536
  if (!this.client || !this.oauthProvider) {
@@ -997,7 +1031,7 @@ export class MCPClient {
997
1031
  }
998
1032
 
999
1033
  await sessions.delete(this.userId, this.sessionId);
1000
- this.disconnect();
1034
+ await this.disconnect();
1001
1035
  }
1002
1036
 
1003
1037
  /**
@@ -1009,10 +1043,29 @@ export class MCPClient {
1009
1043
  }
1010
1044
 
1011
1045
  /**
1012
- * Disconnects from the MCP server and cleans up resources
1013
- * Does not remove session from Redis - use clearSession() for that
1046
+ * Disconnects from the MCP server and cleans up resources.
1047
+ * Does not remove session from Redis use clearSession() for that.
1048
+ *
1049
+ * For Streamable HTTP sessions, sends an HTTP DELETE to the MCP endpoint
1050
+ * before closing, as recommended by the MCP Streamable HTTP spec
1051
+ * (section "Session Management", rule 5). This is best-effort — errors
1052
+ * (e.g. server already restarted, 404/405 responses) are silently ignored.
1014
1053
  */
1015
- disconnect(reason?: string): void {
1054
+ async disconnect(): Promise<void> {
1055
+ // Per the MCP Streamable HTTP spec (2025-11-25), clients SHOULD send an
1056
+ // HTTP DELETE with the mcp-session-id header when they no longer need a
1057
+ // session. The server MAY respond with 405 if it doesn't support explicit
1058
+ // termination — terminateSession() handles that gracefully.
1059
+ // SSEClientTransport has no session concept, so we guard with instanceof.
1060
+ if (this.transport instanceof StreamableHTTPClientTransport) {
1061
+ try {
1062
+ await this.transport.terminateSession();
1063
+ } catch {
1064
+ // Best-effort: server may be unreachable or may have already expired
1065
+ // the session. Either way, we proceed with local cleanup.
1066
+ }
1067
+ }
1068
+
1016
1069
  if (this.client) {
1017
1070
  this.client.close();
1018
1071
  }
@@ -1026,7 +1079,6 @@ export class MCPClient {
1026
1079
  type: 'disconnected',
1027
1080
  sessionId: this.sessionId,
1028
1081
  serverId: this.serverId,
1029
- reason,
1030
1082
  timestamp: Date.now(),
1031
1083
  });
1032
1084
 
@@ -1036,9 +1088,7 @@ export class MCPClient {
1036
1088
  message: `Disconnected from ${this.serverId}`,
1037
1089
  sessionId: this.sessionId,
1038
1090
  serverId: this.serverId,
1039
- payload: {
1040
- reason: reason || 'unknown',
1041
- },
1091
+ payload: {},
1042
1092
  timestamp: Date.now(),
1043
1093
  id: nanoid(),
1044
1094
  });
@@ -121,7 +121,6 @@ export type McpConnectionEvent =
121
121
  type: 'disconnected';
122
122
  sessionId: string;
123
123
  serverId: string;
124
- reason?: string;
125
124
  timestamp: number;
126
125
  }
127
126
  | {
@@ -1,123 +0,0 @@
1
- /**
2
- * Simple event emitter pattern for MCP connection events
3
- * Inspired by Cloudflare's agents pattern but adapted for serverless
4
- */
5
- type Disposable = {
6
- dispose(): void;
7
- };
8
- type Event<T> = (listener: (event: T) => void) => Disposable;
9
- /**
10
- * Event emitter class for type-safe event handling
11
- * Similar to Cloudflare's Emitter but simplified for our use case
12
- */
13
- declare class Emitter<T> {
14
- private listeners;
15
- /**
16
- * Subscribe to events
17
- * @param listener - Callback function to handle events
18
- * @returns Disposable to unsubscribe
19
- */
20
- get event(): Event<T>;
21
- /**
22
- * Fire an event to all listeners
23
- * @param event - Event data to emit
24
- */
25
- fire(event: T): void;
26
- /**
27
- * Clear all listeners
28
- */
29
- dispose(): void;
30
- /**
31
- * Get number of active listeners
32
- */
33
- get listenerCount(): number;
34
- }
35
- /**
36
- * Connection state types matching your existing ConnectionStatus
37
- * Extended with more granular states for better observability
38
- */
39
- type McpConnectionState = 'DISCONNECTED' | 'CONNECTING' | 'AUTHENTICATING' | 'AUTHENTICATED' | 'DISCOVERING' | 'CONNECTED' | 'READY' | 'VALIDATING' | 'RECONNECTING' | 'INITIALIZING' | 'FAILED';
40
- /**
41
- * MCP Connection Event Types
42
- * Discriminated union for type-safe event handling
43
- */
44
- type McpConnectionEvent = {
45
- type: 'state_changed';
46
- sessionId: string;
47
- serverId: string;
48
- serverName: string;
49
- serverUrl: string;
50
- createdAt?: number;
51
- state: McpConnectionState;
52
- previousState: McpConnectionState;
53
- timestamp: number;
54
- } | {
55
- type: 'tools_discovered';
56
- sessionId: string;
57
- serverId: string;
58
- toolCount: number;
59
- tools: any[];
60
- timestamp: number;
61
- } | {
62
- type: 'auth_required';
63
- sessionId: string;
64
- serverId: string;
65
- authUrl: string;
66
- timestamp: number;
67
- } | {
68
- type: 'error';
69
- sessionId: string;
70
- serverId: string;
71
- error: string;
72
- errorType: 'connection' | 'auth' | 'validation' | 'unknown';
73
- timestamp: number;
74
- } | {
75
- type: 'disconnected';
76
- sessionId: string;
77
- serverId: string;
78
- reason?: string;
79
- timestamp: number;
80
- } | {
81
- type: 'progress';
82
- sessionId: string;
83
- serverId: string;
84
- message: string;
85
- timestamp: number;
86
- };
87
- /**
88
- * Event fired when a tool execution returns a UI resource URI
89
- */
90
- interface McpAppsUIEvent {
91
- type: 'mcp-apps-ui';
92
- sessionId: string;
93
- resourceUri: string;
94
- toolName: string;
95
- result: unknown;
96
- timestamp: number;
97
- }
98
- /**
99
- * Observability event for debugging and monitoring
100
- */
101
- interface McpObservabilityEvent {
102
- type?: string;
103
- level?: 'debug' | 'info' | 'warn' | 'error';
104
- message?: string;
105
- displayMessage?: string;
106
- sessionId?: string;
107
- serverId?: string;
108
- payload?: Record<string, any>;
109
- metadata?: Record<string, any>;
110
- timestamp: number;
111
- id?: string;
112
- }
113
- /**
114
- * DisposableStore for managing multiple disposables
115
- * Useful for cleanup in React hooks
116
- */
117
- declare class DisposableStore {
118
- private disposables;
119
- add(disposable: Disposable): void;
120
- dispose(): void;
121
- }
122
-
123
- export { type Disposable as D, Emitter as E, type McpConnectionEvent as M, DisposableStore as a, type Event as b, type McpConnectionState as c, type McpObservabilityEvent as d, type McpAppsUIEvent as e };