@mcp-ts/sdk 2.4.1 → 2.4.3

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 (49) hide show
  1. package/dist/adapters/agui-adapter.d.mts +3 -4
  2. package/dist/adapters/agui-adapter.d.ts +3 -4
  3. package/dist/adapters/agui-middleware.d.mts +3 -4
  4. package/dist/adapters/agui-middleware.d.ts +3 -4
  5. package/dist/adapters/ai-adapter.d.mts +3 -4
  6. package/dist/adapters/ai-adapter.d.ts +3 -4
  7. package/dist/adapters/langchain-adapter.d.mts +3 -4
  8. package/dist/adapters/langchain-adapter.d.ts +3 -4
  9. package/dist/adapters/mastra-adapter.d.mts +2 -2
  10. package/dist/adapters/mastra-adapter.d.ts +2 -2
  11. package/dist/bin/mcp-ts.js +5 -5
  12. package/dist/bin/mcp-ts.js.map +1 -1
  13. package/dist/bin/mcp-ts.mjs +5 -5
  14. package/dist/bin/mcp-ts.mjs.map +1 -1
  15. package/dist/client/index.d.mts +2 -3
  16. package/dist/client/index.d.ts +2 -3
  17. package/dist/client/react.d.mts +4 -6
  18. package/dist/client/react.d.ts +4 -6
  19. package/dist/client/vue.d.mts +4 -6
  20. package/dist/client/vue.d.ts +4 -6
  21. package/dist/{index-Ch7ouNSa.d.ts → index-B8kJSrBJ.d.ts} +1 -2
  22. package/dist/{index-L5XoXgsb.d.mts → index-DiJsm_lK.d.mts} +1 -2
  23. package/dist/index.d.mts +5 -6
  24. package/dist/index.d.ts +5 -6
  25. package/dist/index.js +174 -110
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +174 -110
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/{multi-session-client-k-9RvWvi.d.mts → multi-session-client-B6hsYO8V.d.mts} +218 -71
  30. package/dist/{multi-session-client-Bwm-efqg.d.ts → multi-session-client-DOBvtaQV.d.ts} +218 -71
  31. package/dist/server/index.d.mts +5 -128
  32. package/dist/server/index.d.ts +5 -128
  33. package/dist/server/index.js +174 -110
  34. package/dist/server/index.js.map +1 -1
  35. package/dist/server/index.mjs +174 -110
  36. package/dist/server/index.mjs.map +1 -1
  37. package/dist/shared/index.d.mts +4 -5
  38. package/dist/shared/index.d.ts +4 -5
  39. package/dist/{tool-router-CuApsDiV.d.mts → tool-router-CbG4Tum6.d.mts} +1 -1
  40. package/dist/{tool-router-CZMrOG8J.d.ts → tool-router-ChIhPwgP.d.ts} +1 -1
  41. package/dist/{types-DCk_IF4L.d.mts → types-CjczQwNX.d.mts} +122 -1
  42. package/dist/{types-DCk_IF4L.d.ts → types-CjczQwNX.d.ts} +122 -1
  43. package/package.json +1 -1
  44. package/src/bin/mcp-ts.ts +5 -5
  45. package/src/server/mcp/multi-session-client.ts +190 -132
  46. package/src/server/mcp/oauth-client.ts +49 -7
  47. package/src/server/storage/index.ts +8 -8
  48. package/dist/events-C4m7tK1U.d.mts +0 -122
  49. package/dist/events-C4m7tK1U.d.ts +0 -122
@@ -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,123 @@ 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
+
63
103
  /**
64
- * Fetches all sessions for this userId from storage and returns only the
65
- * ones that are ready to connect.
104
+ * Fetches active sessions and establishes connections to all of them.
66
105
  *
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.
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.
71
109
  */
72
- private async getActiveSessions(): Promise<Session[]> {
73
- const sessionList = await sessions.list(this.userId);
74
- const valid = sessionList.filter(s =>
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
+
155
+ /**
156
+ * Removes and disconnects a single session by ID.
157
+ *
158
+ * @returns `true` if the session was found and removed, `false` if not found.
159
+ */
160
+ async removeSession(sessionId: string): Promise<boolean> {
161
+ const idx = this.clients.findIndex(c => c.getSessionId() === sessionId);
162
+ if (idx === -1) return false;
163
+ const [client] = this.clients.splice(idx, 1);
164
+ await client.disconnect();
165
+ return true;
166
+ }
167
+
168
+ /**
169
+ * Gracefully disconnects all active MCP clients and clears the internal list.
170
+ *
171
+ * For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
172
+ * endpoint per the spec before closing locally. All disconnects run in
173
+ * parallel so shutdown is not serialised across many sessions.
174
+ *
175
+ * Call this during server shutdown or when a user logs out to free up
176
+ * underlying transport resources (SSE streams, HTTP connections, etc.).
177
+ */
178
+ async disconnect(): Promise<void> {
179
+ await Promise.all(this.clients.map((client) => client.disconnect()));
180
+ this.clients = [];
181
+ }
182
+
183
+ // -----------------------------------------------------------------------
184
+ // Internals
185
+ // -----------------------------------------------------------------------
186
+
187
+ /**
188
+ * Resolves the list of sessions to connect.
189
+ *
190
+ * Uses the custom `sessionProvider` when provided, otherwise falls back
191
+ * to querying the storage backend via `sessions.list(userId)`.
192
+ */
193
+ private async fetchActiveSessions(): Promise<Session[]> {
194
+ const sessionList = this.options.sessionProvider
195
+ ? await this.options.sessionProvider()
196
+ : await sessions.list(this.userId);
197
+
198
+ return sessionList.filter(s =>
75
199
  s.serverId &&
76
200
  s.serverUrl &&
77
201
  s.callbackUrl &&
78
202
  s.status === 'active'
79
203
  );
80
- return valid;
81
204
  }
82
205
 
83
206
  /**
84
- * Connects to a list of sessions in controlled batches of `CONNECTION_BATCH_SIZE`.
207
+ * Connects a list of sessions in controlled batches.
85
208
  *
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.
209
+ * Batching prevents overwhelming the event loop or external servers when
210
+ * a user has many active MCP sessions. Within each batch, sessions are
211
+ * connected concurrently using `Promise.all`.
89
212
  */
90
213
  private async connectInBatches(sessions: Session[]): Promise<void> {
91
214
  for (let i = 0; i < sessions.length; i += CONNECTION_BATCH_SIZE) {
@@ -94,38 +217,30 @@ export class MultiSessionClient {
94
217
  }
95
218
  }
96
219
 
97
- private connectionPromises = new Map<string, Promise<void>>();
98
-
99
220
  /**
100
- * Connects a single session, with built-in deduplication to prevent race conditions.
221
+ * Connects a single session, with deduplication to prevent race conditions.
101
222
  *
102
- * - If a client for this session already exists and is connected, returns immediately.
103
- * - If the existing client entry is no longer connected (e.g. it was explicitly
104
- * disconnected), it is evicted so that `establishConnectionWithRetries` creates a
105
- * fresh transport preventing "Client already connected" errors from the SDK.
106
- * - If a connection attempt for this session is already in-flight (e.g. from a
107
- * concurrent call), it joins the existing promise instead of starting a new one.
108
- * This is the key concurrency lock the `connectionPromises` map acts as a
109
- * per-session mutex so we never spin up two physical connections for the same session.
110
- * - On completion (success or failure), the promise is cleaned up from the map.
223
+ * - If a client for this session already exists and is connected, returns
224
+ * immediately.
225
+ * - If the existing client entry is no longer connected (e.g. explicit
226
+ * disconnect), it is evicted so a fresh transport is created.
227
+ * - If a connection attempt for this session is already in-flight, the
228
+ * existing promise is reused as a per-session mutex.
229
+ * - On completion (success or failure), the promise is cleaned up from
230
+ * the connectionPromises map.
111
231
  */
112
232
  private async connectSession(session: Session): Promise<void> {
113
233
  const existing = this.clients.find(c => c.getSessionId() === session.sessionId);
114
234
 
115
235
  if (existing) {
116
236
  if (existing.isConnected()) {
117
- // Genuinely connected — nothing to do.
118
237
  return;
119
238
  }
120
239
 
121
- // Client entry exists but is no longer connected (explicit disconnect or
122
- // a prior failed reconnect attempt). Remove it so the fresh connect below
123
- // starts with a clean slate and the underlying SDK Client doesn't complain
124
- // about an already-attached transport.
240
+ this.options.onSessionEvicted?.(existing.getSessionId());
125
241
  this.clients = this.clients.filter(c => c !== existing);
126
242
  }
127
243
 
128
- // Avoid concurrent connection attempts for the same session
129
244
  if (this.connectionPromises.has(session.sessionId)) {
130
245
  return this.connectionPromises.get(session.sessionId)!;
131
246
  }
@@ -142,22 +257,19 @@ export class MultiSessionClient {
142
257
  }
143
258
 
144
259
  /**
145
- * The core connection loop for a single session.
260
+ * Core connection loop for a single session with retry logic.
146
261
  *
147
- * Attempts to establish a physical MCP connection, retrying up to `maxRetries` times
148
- * if the connection fails. Each attempt:
149
- * 1. Creates a fresh `MCPClient` instance from the session data.
150
- * 2. Races the connect call against a timeout promise — if the server doesn't respond
151
- * within `timeoutMs`, the attempt is aborted and counted as a failure.
152
- * 3. On success, replaces any stale client entry for this session in the `clients` array.
262
+ * 1. Creates a fresh `MCPClient` from the session data.
263
+ * 2. Races `client.connect()` against a timeout.
264
+ * 3. On success, replaces any stale entry and fires `onSessionConnected`.
153
265
  * 4. On failure, waits `retryDelay` ms before the next attempt.
154
266
  *
155
- * If all attempts are exhausted, logs an error and returns silently (does not throw),
156
- * so a single bad server doesn't block the rest of the batch from connecting.
267
+ * If all attempts are exhausted, logs an error and returns silently so
268
+ * a single bad server doesn't block the rest of the batch.
157
269
  */
158
270
  private async establishConnectionWithRetries(session: Session): Promise<void> {
159
- const maxRetries = this.options.maxRetries ?? DEFAULT_MAX_RETRIES;
160
- const retryDelay = this.options.retryDelay ?? DEFAULT_RETRY_DELAY_MS;
271
+ const maxRetries = this.options.maxRetries;
272
+ const retryDelay = this.options.retryDelay;
161
273
  let lastError: unknown;
162
274
 
163
275
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
@@ -173,10 +285,13 @@ export class MultiSessionClient {
173
285
  headers: session.headers,
174
286
  });
175
287
 
176
- const timeoutMs = this.options.timeout ?? DEFAULT_TIMEOUT_MS;
288
+ const timeoutMs = this.options.timeout;
177
289
  let timeoutTimer: ReturnType<typeof setTimeout>;
178
290
  const timeoutPromise = new Promise<never>((_, reject) => {
179
- timeoutTimer = setTimeout(() => reject(new Error(`Connection timed out after ${timeoutMs}ms`)), timeoutMs);
291
+ timeoutTimer = setTimeout(
292
+ () => reject(new Error(`Connection timed out after ${timeoutMs}ms`)),
293
+ timeoutMs,
294
+ );
180
295
  });
181
296
 
182
297
  try {
@@ -185,10 +300,11 @@ export class MultiSessionClient {
185
300
  clearTimeout(timeoutTimer!);
186
301
  }
187
302
 
188
- // Always replace the disconnected client entry
189
303
  this.clients = this.clients.filter(c => c.getSessionId() !== session.sessionId);
190
304
  this.clients.push(client);
191
- return; // successfully connected
305
+ this.options.onSessionConnected?.(session.sessionId, client);
306
+ return;
307
+
192
308
  } catch (error) {
193
309
  lastError = error;
194
310
  if (attempt < maxRetries) {
@@ -197,69 +313,11 @@ export class MultiSessionClient {
197
313
  }
198
314
  }
199
315
 
200
- console.error(`[MultiSessionClient] Failed to connect to session ${session.sessionId} after ${maxRetries + 1} attempts:`, lastError);
201
- }
202
-
203
- /**
204
- * The main entry point. Fetches all active sessions for this userId from
205
- * storage and establishes connections to all of them in batches.
206
- *
207
- * Call this once after creating the client. On traditional servers, you can
208
- * cache the `MultiSessionClient` instance after calling `connect()` to avoid
209
- * re-fetching and re-connecting on every request.
210
- */
211
- async connect(): Promise<void> {
212
- const sessions = await this.getActiveSessions();
213
- const activeSessionIds = new Set(sessions.map(s => s.sessionId));
214
-
215
- // Prune stale clients whose sessions no longer exist in storage.
216
- // Otherwise a lingering MCPClient with a still-alive SDK Client
217
- // transport triggers OAuth refreshes on listTools(), and the
218
- // StorageOAuthClientProvider.state() -> patchCredentials() call
219
- // fails with a FK violation because the session was deleted.
220
- this.clients = this.clients.filter(c => activeSessionIds.has(c.getSessionId()));
221
-
222
- await this.connectInBatches(sessions);
223
- }
224
-
225
- /**
226
- * Drops all cached `MCPClient` instances and reconnects fresh from storage.
227
- *
228
- * Call this when downstream MCP servers have expired their transport sessions
229
- * (e.g. after a remote server restart) and subsequent tool calls return
230
- * "Session not found. Reconnect without session header." errors.
231
- *
232
- * OAuth tokens are preserved in the storage backend — no re-authentication
233
- * is required. Only the in-memory transport sessions are cleared.
234
- */
235
- async reconnect(): Promise<void> {
236
- await this.disconnect();
237
- await this.connect();
238
- }
239
-
240
- /**
241
- * Returns all currently connected `MCPClient` instances.
242
- *
243
- * Use this to enumerate available tools across all connected servers,
244
- * or to route a tool call to the right client by `serverId`.
245
- */
246
- getClients(): MCPClient[] {
247
- return this.clients;
248
- }
249
-
250
- /**
251
- * Gracefully disconnects all active MCP clients and clears the internal client list.
252
- *
253
- * For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
254
- * endpoint per the spec before closing locally. All disconnects run in
255
- * parallel so shutdown is not serialised across many sessions.
256
- *
257
- * Call this during server shutdown or when a user logs out to free up
258
- * underlying transport resources (SSE streams, HTTP connections, etc.).
259
- */
260
- async disconnect(): Promise<void> {
261
- await Promise.all(this.clients.map((client) => client.disconnect()));
262
- this.clients = [];
316
+ this.options.onSessionFailed?.(session.sessionId, lastError);
317
+ console.error(
318
+ `[MultiSessionClient] Failed to connect to session ${session.sessionId} ` +
319
+ `after ${maxRetries + 1} attempts:`,
320
+ lastError,
321
+ );
263
322
  }
264
323
  }
265
-
@@ -257,7 +257,6 @@ export class MCPClient {
257
257
  const hasSessionHeader = init?.headers && new Headers(init.headers as HeadersInit).has('mcp-session-id');
258
258
 
259
259
  if (response.status === 404 && hasSessionHeader) {
260
- this.client = null;
261
260
  throw new Error("MCP_SESSION_EXPIRED: Downstream session was not found on the server.");
262
261
  }
263
262
 
@@ -791,7 +790,9 @@ export class MCPClient {
791
790
  params: {},
792
791
  };
793
792
 
794
- const result = await this.client.request(request, ListToolsResultSchema);
793
+ const result = await this.withRetry(() =>
794
+ this.client!.request(request, ListToolsResultSchema)
795
+ );
795
796
 
796
797
  if (this.serverId) {
797
798
  this._onConnectionEvent.fire({
@@ -837,7 +838,9 @@ export class MCPClient {
837
838
  };
838
839
 
839
840
  try {
840
- const result = await this.client.request(request, CallToolResultSchema);
841
+ const result = await this.withRetry(() =>
842
+ this.client!.request(request, CallToolResultSchema)
843
+ );
841
844
 
842
845
  this._onObservabilityEvent.fire({
843
846
  type: 'mcp:client:tool_call',
@@ -897,7 +900,9 @@ export class MCPClient {
897
900
  params: {},
898
901
  };
899
902
 
900
- const result = await this.client.request(request, ListPromptsResultSchema);
903
+ const result = await this.withRetry(() =>
904
+ this.client!.request(request, ListPromptsResultSchema)
905
+ );
901
906
 
902
907
  this.emitStateChange('READY');
903
908
  this.emitProgress(`Discovered ${result.prompts.length} prompts`);
@@ -931,7 +936,9 @@ export class MCPClient {
931
936
  },
932
937
  };
933
938
 
934
- return await this.client.request(request, GetPromptResultSchema);
939
+ return await this.withRetry(() =>
940
+ this.client!.request(request, GetPromptResultSchema)
941
+ );
935
942
  }
936
943
 
937
944
  /**
@@ -952,7 +959,9 @@ export class MCPClient {
952
959
  params: {},
953
960
  };
954
961
 
955
- const result = await this.client.request(request, ListResourcesResultSchema);
962
+ const result = await this.withRetry(() =>
963
+ this.client!.request(request, ListResourcesResultSchema)
964
+ );
956
965
 
957
966
  this.emitStateChange('READY');
958
967
  this.emitProgress(`Discovered ${result.resources.length} resources`);
@@ -984,7 +993,34 @@ export class MCPClient {
984
993
  },
985
994
  };
986
995
 
987
- return await this.client.request(request, ReadResourceResultSchema);
996
+ return await this.withRetry(() =>
997
+ this.client!.request(request, ReadResourceResultSchema)
998
+ );
999
+ }
1000
+
1001
+ /**
1002
+ * Wraps an MCP request with automatic transport-session recovery.
1003
+ *
1004
+ * When the downstream MCP server rejects the request with a 404 indicating
1005
+ * the transport session has expired, this method tears down the stale SDK
1006
+ * client and transport, calls {@link reconnect} to negotiate a fresh session,
1007
+ * and retries the request once.
1008
+ *
1009
+ * Non-transient errors (network failures, auth errors, etc.) propagate as-is.
1010
+ */
1011
+ private async withRetry<T>(fn: () => Promise<T>): Promise<T> {
1012
+ try {
1013
+ return await fn();
1014
+ } catch (error) {
1015
+ if (!(error instanceof Error && error.message.includes('MCP_SESSION_EXPIRED'))) throw error;
1016
+ if (this.client) {
1017
+ try { await this.client.close(); } catch {}
1018
+ this.transport = null;
1019
+ this.client = null;
1020
+ }
1021
+ await this.reconnect();
1022
+ return await fn();
1023
+ }
988
1024
  }
989
1025
 
990
1026
  /**
@@ -1000,6 +1036,12 @@ export class MCPClient {
1000
1036
  throw new Error('OAuth provider not initialized');
1001
1037
  }
1002
1038
 
1039
+ // Close the client initialize() may have created — we need a fresh
1040
+ // client that will negotiate a new transport session.
1041
+ if (this.client) {
1042
+ try { await this.client.close(); } catch {}
1043
+ }
1044
+
1003
1045
  this.client = new Client(
1004
1046
  {
1005
1047
  name: MCP_CLIENT_NAME,
@@ -141,13 +141,13 @@ async function createStorage(): Promise<SessionStore> {
141
141
 
142
142
  if (type === 'supabase') {
143
143
  const url = process.env.SUPABASE_URL;
144
- const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_ANON_KEY;
144
+ const key = process.env.SUPABASE_SECRET_KEY || process.env.SUPABASE_ANON_KEY;
145
145
 
146
146
  if (!url || !key) {
147
- console.warn('[mcp-ts][Storage] Explicit selection "supabase" requires SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY.');
147
+ console.warn('[mcp-ts][Storage] Explicit selection "supabase" requires SUPABASE_URL and SUPABASE_SECRET_KEY.');
148
148
  } else {
149
- if (!process.env.SUPABASE_SERVICE_ROLE_KEY) {
150
- console.warn('[mcp-ts][Storage] ⚠️ Warning: Using "SUPABASE_ANON_KEY" for server-side storage. You may encounter RLS policy violations. "SUPABASE_SERVICE_ROLE_KEY" is recommended.');
149
+ if (!process.env.SUPABASE_SECRET_KEY) {
150
+ console.warn('[mcp-ts][Storage] ⚠️ Warning: Using "SUPABASE_ANON_KEY" for server-side storage. You may encounter RLS policy violations. "SUPABASE_SECRET_KEY" is recommended.');
151
151
  }
152
152
  try {
153
153
  const { createClient } = await import('@supabase/supabase-js');
@@ -210,14 +210,14 @@ async function createStorage(): Promise<SessionStore> {
210
210
  return await initializeStorage(new SqliteStorage({ path: process.env.MCP_TS_STORAGE_SQLITE_PATH }));
211
211
  }
212
212
 
213
- if (process.env.SUPABASE_URL && (process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_ANON_KEY)) {
213
+ if (process.env.SUPABASE_URL && (process.env.SUPABASE_SECRET_KEY || process.env.SUPABASE_ANON_KEY)) {
214
214
  try {
215
215
  const { createClient } = await import('@supabase/supabase-js');
216
216
  const url = process.env.SUPABASE_URL;
217
- const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_ANON_KEY!;
217
+ const key = process.env.SUPABASE_SECRET_KEY || process.env.SUPABASE_ANON_KEY!;
218
218
 
219
- if (!process.env.SUPABASE_SERVICE_ROLE_KEY) {
220
- console.warn('[mcp-ts][Storage] ⚠️ Warning: Using "SUPABASE_ANON_KEY" for server-side storage. You may encounter RLS policy violations. "SUPABASE_SERVICE_ROLE_KEY" is recommended.');
219
+ if (!process.env.SUPABASE_SECRET_KEY) {
220
+ console.warn('[mcp-ts][Storage] ⚠️ Warning: Using "SUPABASE_ANON_KEY" for server-side storage. You may encounter RLS policy violations. "SUPABASE_SECRET_KEY" is recommended.');
221
221
  }
222
222
 
223
223
  const client = createClient(url, key);