@mcp-ts/sdk 2.3.4 → 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 (89) hide show
  1. package/README.md +161 -39
  2. package/dist/adapters/agui-adapter.d.mts +4 -4
  3. package/dist/adapters/agui-adapter.d.ts +4 -4
  4. package/dist/adapters/agui-middleware.d.mts +4 -4
  5. package/dist/adapters/agui-middleware.d.ts +4 -4
  6. package/dist/adapters/ai-adapter.d.mts +4 -4
  7. package/dist/adapters/ai-adapter.d.ts +4 -4
  8. package/dist/adapters/langchain-adapter.d.mts +4 -4
  9. package/dist/adapters/langchain-adapter.d.ts +4 -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 +3 -3
  13. package/dist/client/index.d.ts +3 -3
  14. package/dist/client/index.js +2 -2
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/index.mjs +2 -2
  17. package/dist/client/index.mjs.map +1 -1
  18. package/dist/client/react.d.mts +11 -10
  19. package/dist/client/react.d.ts +11 -10
  20. package/dist/client/react.js +66 -26
  21. package/dist/client/react.js.map +1 -1
  22. package/dist/client/react.mjs +67 -27
  23. package/dist/client/react.mjs.map +1 -1
  24. package/dist/client/vue.d.mts +8 -7
  25. package/dist/client/vue.d.ts +8 -7
  26. package/dist/client/vue.js +27 -17
  27. package/dist/client/vue.js.map +1 -1
  28. package/dist/client/vue.mjs +27 -17
  29. package/dist/client/vue.mjs.map +1 -1
  30. package/dist/{events-CK3N--3g.d.mts → events-C4m7tK1U.d.mts} +0 -1
  31. package/dist/{events-CK3N--3g.d.ts → events-C4m7tK1U.d.ts} +0 -1
  32. package/dist/{index-CmjMd2ac.d.ts → index-Ch7ouNSa.d.ts} +3 -3
  33. package/dist/{index-Cfjsme-a.d.mts → index-L5XoXgsb.d.mts} +3 -3
  34. package/dist/index.d.mts +6 -6
  35. package/dist/index.d.ts +6 -6
  36. package/dist/index.js +799 -326
  37. package/dist/index.js.map +1 -1
  38. package/dist/index.mjs +793 -324
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/{multi-session-client-C_kPHpD5.d.ts → multi-session-client-Bwm-efqg.d.ts} +43 -38
  41. package/dist/{multi-session-client-C7hGqzgM.d.mts → multi-session-client-k-9RvWvi.d.mts} +43 -38
  42. package/dist/server/index.d.mts +48 -20
  43. package/dist/server/index.d.ts +48 -20
  44. package/dist/server/index.js +791 -320
  45. package/dist/server/index.js.map +1 -1
  46. package/dist/server/index.mjs +787 -319
  47. package/dist/server/index.mjs.map +1 -1
  48. package/dist/shared/index.d.mts +10 -9
  49. package/dist/shared/index.d.ts +10 -9
  50. package/dist/shared/index.js +7 -5
  51. package/dist/shared/index.js.map +1 -1
  52. package/dist/shared/index.mjs +5 -4
  53. package/dist/shared/index.mjs.map +1 -1
  54. package/dist/{tool-router-BMzhoNYt.d.ts → tool-router-CZMrOG8J.d.ts} +1 -1
  55. package/dist/{tool-router-BhHsvBfP.d.mts → tool-router-CuApsDiV.d.mts} +1 -1
  56. package/dist/{types-CxFaaZrM.d.mts → types-DCk_IF4L.d.mts} +5 -3
  57. package/dist/{types-CxFaaZrM.d.ts → types-DCk_IF4L.d.ts} +5 -3
  58. package/migrations/neon/20260513010000_install_mcp_sessions.sql +40 -3
  59. package/migrations/neon/20260513020000_add_session_cleanup_cron.sql +4 -4
  60. package/migrations/supabase/20260330195700_install_mcp_sessions.sql +67 -3
  61. package/migrations/supabase/20260421010000_add_session_cleanup_cron.sql +6 -6
  62. package/migrations/v2.3.4/neon/20260513010000_install_mcp_sessions.sql +69 -0
  63. package/migrations/v2.3.4/neon/20260513020000_add_session_cleanup_cron.sql +35 -0
  64. package/migrations/v2.3.4/supabase/20260330195700_install_mcp_sessions.sql +82 -0
  65. package/migrations/v2.3.4/supabase/20260421010000_add_session_cleanup_cron.sql +32 -0
  66. package/package.json +13 -3
  67. package/src/client/core/sse-client.ts +2 -2
  68. package/src/client/react/index.ts +1 -1
  69. package/src/client/react/oauth-popup.tsx +34 -13
  70. package/src/client/react/use-mcp.ts +19 -45
  71. package/src/client/utils/session-state.ts +43 -0
  72. package/src/client/vue/use-mcp.ts +18 -47
  73. package/src/server/handlers/sse-handler.ts +16 -9
  74. package/src/server/mcp/multi-session-client.ts +48 -11
  75. package/src/server/mcp/oauth-client.ts +133 -111
  76. package/src/server/mcp/storage-oauth-provider.ts +79 -50
  77. package/src/server/storage/file-backend.ts +74 -18
  78. package/src/server/storage/index.ts +2 -4
  79. package/src/server/storage/memory-backend.ts +49 -13
  80. package/src/server/storage/neon-backend.ts +201 -68
  81. package/src/server/storage/redis-backend.ts +81 -23
  82. package/src/server/storage/session-lifecycle.ts +78 -0
  83. package/src/server/storage/sqlite-backend.ts +89 -15
  84. package/src/server/storage/supabase-backend.ts +188 -63
  85. package/src/server/storage/types.ts +49 -16
  86. package/src/shared/constants.ts +5 -6
  87. package/src/shared/events.ts +0 -1
  88. package/src/shared/types.ts +5 -3
  89. package/src/shared/utils.ts +26 -0
@@ -156,22 +156,9 @@ init_cjs_shims();
156
156
  // src/server/storage/redis-backend.ts
157
157
  init_cjs_shims();
158
158
 
159
- // src/shared/constants.ts
160
- init_cjs_shims();
161
- var SESSION_TTL_SECONDS = 43200;
162
- var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
163
- var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
164
- var DEFAULT_CLIENT_NAME = "MCP Assistant";
165
- var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
166
- var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
167
- var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
168
- var SOFTWARE_ID = "@mcp-ts";
169
- var SOFTWARE_VERSION = "1.3.4";
170
- var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
171
- var MCP_CLIENT_VERSION = "2.0";
172
-
173
159
  // src/shared/utils.ts
174
160
  init_cjs_shims();
161
+ var OAUTH_STATE_SEPARATOR = ".";
175
162
  var firstChar = nanoid.customAlphabet(
176
163
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
177
164
  1
@@ -190,13 +177,99 @@ function sanitizeServerLabel(name) {
190
177
  function generateSessionId() {
191
178
  return firstChar() + rest();
192
179
  }
180
+ function formatOAuthState(nonce, sessionId) {
181
+ return `${nonce}${OAUTH_STATE_SEPARATOR}${sessionId}`;
182
+ }
183
+ function parseOAuthState(state) {
184
+ const separatorIndex = state.indexOf(OAUTH_STATE_SEPARATOR);
185
+ if (separatorIndex <= 0 || separatorIndex === state.length - 1) {
186
+ return void 0;
187
+ }
188
+ const nonce = state.slice(0, separatorIndex);
189
+ const sessionId = state.slice(separatorIndex + 1);
190
+ if (!nonce || !sessionId || sessionId.includes(OAUTH_STATE_SEPARATOR)) {
191
+ return void 0;
192
+ }
193
+ return { nonce, sessionId };
194
+ }
195
+
196
+ // src/server/storage/session-lifecycle.ts
197
+ init_cjs_shims();
198
+
199
+ // src/shared/constants.ts
200
+ init_cjs_shims();
201
+ var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
202
+ var DORMANT_SESSION_EXPIRATION_MS = 30 * 24 * 60 * 60 * 1e3;
203
+ var DORMANT_SESSION_EXPIRATION_SECONDS = Math.floor(DORMANT_SESSION_EXPIRATION_MS / 1e3);
204
+ var DEFAULT_CLIENT_NAME = "MCP Assistant";
205
+ var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
206
+ var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
207
+ var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
208
+ var SOFTWARE_ID = "@mcp-ts";
209
+ var SOFTWARE_VERSION = "2.3.4";
210
+ var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
211
+ var MCP_CLIENT_VERSION = "2.0";
212
+
213
+ // src/server/storage/session-lifecycle.ts
214
+ function resolveSessionExpiresAt(status = "pending", referenceTime = Date.now()) {
215
+ return status === "active" ? null : referenceTime + STATE_EXPIRATION_MS;
216
+ }
217
+ function resolveSessionRedisTtlSeconds(session) {
218
+ return session.status === "active" ? DORMANT_SESSION_EXPIRATION_SECONDS : Math.floor(STATE_EXPIRATION_MS / 1e3);
219
+ }
220
+ function normalizeNewSession(session, now = Date.now()) {
221
+ const createdAt = session.createdAt || now;
222
+ const updatedAt = session.updatedAt ?? createdAt;
223
+ const status = session.status ?? "pending";
224
+ return {
225
+ ...session,
226
+ status,
227
+ createdAt,
228
+ updatedAt,
229
+ expiresAt: resolveSessionExpiresAt(status, status === "active" ? updatedAt : createdAt)
230
+ };
231
+ }
232
+ function mergeSessionUpdate(current, data, now = Date.now()) {
233
+ const updatedAt = data.updatedAt ?? now;
234
+ const updated = {
235
+ ...current,
236
+ ...data,
237
+ updatedAt
238
+ };
239
+ const status = updated.status ?? "pending";
240
+ return {
241
+ ...updated,
242
+ status,
243
+ expiresAt: resolveSessionExpiresAt(status, updatedAt)
244
+ };
245
+ }
246
+ function normalizeStoredSession(session) {
247
+ const createdAt = session.createdAt || Date.now();
248
+ const updatedAt = session.updatedAt ?? createdAt;
249
+ const status = session.status ?? "pending";
250
+ const expiresAt = status === "active" ? null : session.expiresAt ?? resolveSessionExpiresAt(status, createdAt);
251
+ return {
252
+ ...session,
253
+ status,
254
+ createdAt,
255
+ updatedAt,
256
+ expiresAt
257
+ };
258
+ }
259
+ function isSessionExpired(session, now = Date.now()) {
260
+ const hydrated = normalizeStoredSession(session);
261
+ if (hydrated.status === "active") {
262
+ return hydrated.updatedAt !== void 0 && hydrated.updatedAt < now - DORMANT_SESSION_EXPIRATION_MS;
263
+ }
264
+ return typeof hydrated.expiresAt === "number" && hydrated.expiresAt < now;
265
+ }
193
266
 
194
267
  // src/server/storage/redis-backend.ts
195
268
  var RedisStorageBackend = class {
196
269
  constructor(redis2) {
197
270
  this.redis = redis2;
198
- __publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
199
271
  __publicField(this, "KEY_PREFIX", "mcp:session:");
272
+ __publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
200
273
  __publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
201
274
  __publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
202
275
  }
@@ -215,6 +288,9 @@ var RedisStorageBackend = class {
215
288
  getSessionKey(userId, sessionId) {
216
289
  return `${this.KEY_PREFIX}${userId}:${sessionId}`;
217
290
  }
291
+ getCredentialsKey(userId, sessionId) {
292
+ return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
293
+ }
218
294
  /**
219
295
  * Generates Redis key for tracking all sessions for a user
220
296
  * @private
@@ -252,15 +328,16 @@ var RedisStorageBackend = class {
252
328
  generateSessionId() {
253
329
  return generateSessionId();
254
330
  }
255
- async create(session, ttl) {
331
+ async create(session) {
256
332
  const { sessionId, userId } = session;
257
333
  if (!sessionId || !userId) throw new Error("userId and sessionId required");
258
334
  const sessionKey = this.getSessionKey(userId, sessionId);
259
335
  const userIdKey = this.getUserIdKey(userId);
260
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
336
+ const sessionWithLifecycle = normalizeNewSession(session);
337
+ const effectiveTtl = resolveSessionRedisTtlSeconds(sessionWithLifecycle);
261
338
  const result = await this.redis.set(
262
339
  sessionKey,
263
- JSON.stringify(session),
340
+ JSON.stringify(sessionWithLifecycle),
264
341
  "EX",
265
342
  effectiveTtl,
266
343
  "NX"
@@ -270,9 +347,8 @@ var RedisStorageBackend = class {
270
347
  }
271
348
  await this.redis.sadd(userIdKey, sessionId);
272
349
  }
273
- async update(userId, sessionId, data, ttl) {
350
+ async update(userId, sessionId, data) {
274
351
  const sessionKey = this.getSessionKey(userId, sessionId);
275
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
276
352
  const script = `
277
353
  local currentStr = redis.call("GET", KEYS[1])
278
354
  if not currentStr then
@@ -280,26 +356,47 @@ var RedisStorageBackend = class {
280
356
  end
281
357
 
282
358
  local current = cjson.decode(currentStr)
283
- local updates = cjson.decode(ARGV[1])
284
-
285
- for k,v in pairs(updates) do
286
- current[k] = v
287
- end
359
+ local updated = cjson.decode(ARGV[1])
288
360
 
289
- redis.call("SET", KEYS[1], cjson.encode(current), "EX", ARGV[2])
361
+ redis.call("SET", KEYS[1], cjson.encode(updated), "EX", ARGV[2])
290
362
  return 1
291
363
  `;
364
+ const current = await this.get(userId, sessionId);
365
+ if (!current) {
366
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
367
+ }
368
+ const updated = mergeSessionUpdate(current, data);
369
+ const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
292
370
  const result = await this.redis.eval(
293
371
  script,
294
372
  1,
295
373
  sessionKey,
296
- JSON.stringify(data),
374
+ JSON.stringify(updated),
297
375
  effectiveTtl
298
376
  );
299
377
  if (result === 0) {
300
378
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
301
379
  }
302
380
  }
381
+ async patchCredentials(userId, sessionId, data) {
382
+ const sessionKey = this.getSessionKey(userId, sessionId);
383
+ const credentialsKey = this.getCredentialsKey(userId, sessionId);
384
+ const sessionExists = await this.redis.exists(sessionKey);
385
+ if (!sessionExists) {
386
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
387
+ }
388
+ const session = await this.get(userId, sessionId);
389
+ const currentTtl = await this.redis.ttl(sessionKey);
390
+ const effectiveTtl = currentTtl > 0 && session ? currentTtl : resolveSessionRedisTtlSeconds(session ?? { status: "pending" });
391
+ const currentStr = await this.redis.get(credentialsKey);
392
+ const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
393
+ const credentials = { ...current, ...data, sessionId, userId };
394
+ if (effectiveTtl > 0) {
395
+ await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
396
+ } else {
397
+ await this.redis.set(credentialsKey, JSON.stringify(credentials));
398
+ }
399
+ }
303
400
  async get(userId, sessionId) {
304
401
  try {
305
402
  const sessionKey = this.getSessionKey(userId, sessionId);
@@ -307,13 +404,28 @@ var RedisStorageBackend = class {
307
404
  if (!sessionDataStr) {
308
405
  return null;
309
406
  }
310
- const Session = JSON.parse(sessionDataStr);
311
- return Session;
407
+ const session = JSON.parse(sessionDataStr);
408
+ return normalizeStoredSession(session);
312
409
  } catch (error) {
313
410
  console.error("[RedisStorageBackend] Failed to get session:", error);
314
411
  return null;
315
412
  }
316
413
  }
414
+ async getCredentials(userId, sessionId) {
415
+ const session = await this.get(userId, sessionId);
416
+ if (!session) return null;
417
+ const credentialsStr = await this.redis.get(this.getCredentialsKey(userId, sessionId));
418
+ return credentialsStr ? JSON.parse(credentialsStr) : { sessionId, userId };
419
+ }
420
+ async clearCredentials(userId, sessionId) {
421
+ await this.patchCredentials(userId, sessionId, {
422
+ clientInformation: null,
423
+ tokens: null,
424
+ codeVerifier: null,
425
+ clientId: null,
426
+ oauthState: null
427
+ });
428
+ }
317
429
  async listIds(userId) {
318
430
  const sessions2 = await this.list(userId);
319
431
  return sessions2.map((session) => session.sessionId);
@@ -333,7 +445,7 @@ var RedisStorageBackend = class {
333
445
  if (staleSessionIds.length > 0) {
334
446
  await this.redis.srem(userIdKey, ...staleSessionIds);
335
447
  }
336
- return results.filter((session) => session !== null);
448
+ return results.filter((session) => session !== null).map((session) => normalizeStoredSession(session));
337
449
  } catch (error) {
338
450
  console.error(`[RedisStorageBackend] Failed to get session data for ${userId}:`, error);
339
451
  return [];
@@ -342,9 +454,10 @@ var RedisStorageBackend = class {
342
454
  async delete(userId, sessionId) {
343
455
  try {
344
456
  const sessionKey = this.getSessionKey(userId, sessionId);
457
+ const credentialsKey = this.getCredentialsKey(userId, sessionId);
345
458
  const userIdKey = this.getUserIdKey(userId);
346
459
  await this.redis.srem(userIdKey, sessionId);
347
- await this.redis.del(sessionKey);
460
+ await this.redis.del(sessionKey, credentialsKey);
348
461
  } catch (error) {
349
462
  console.error("[RedisStorageBackend] Failed to remove session:", error);
350
463
  }
@@ -359,7 +472,7 @@ var RedisStorageBackend = class {
359
472
  return null;
360
473
  }
361
474
  try {
362
- return JSON.parse(data).sessionId;
475
+ return normalizeStoredSession(JSON.parse(data)).sessionId;
363
476
  } catch (error) {
364
477
  console.error("[RedisStorageBackend] Failed to parse session while listing all session IDs:", error);
365
478
  return null;
@@ -375,8 +488,9 @@ var RedisStorageBackend = class {
375
488
  async clearAll() {
376
489
  try {
377
490
  const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
491
+ const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
378
492
  const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
379
- const allKeys = [...keys, ...userIdKeys];
493
+ const allKeys = [...keys, ...credentialKeys, ...userIdKeys];
380
494
  if (allKeys.length > 0) {
381
495
  await this.redis.del(...allKeys);
382
496
  }
@@ -400,6 +514,7 @@ var RedisStorageBackend = class {
400
514
  const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
401
515
  if (staleSessionIds.length > 0) {
402
516
  await this.redis.srem(userIdKey, ...staleSessionIds);
517
+ await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
403
518
  }
404
519
  const remainingCount = await this.redis.scard(userIdKey);
405
520
  if (remainingCount === 0) {
@@ -425,6 +540,7 @@ var MemoryStorageBackend = class {
425
540
  constructor() {
426
541
  // Map<userId:sessionId, Session>
427
542
  __publicField(this, "sessions", /* @__PURE__ */ new Map());
543
+ __publicField(this, "credentials", /* @__PURE__ */ new Map());
428
544
  // Map<userId, Set<sessionId>>
429
545
  __publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
430
546
  }
@@ -437,36 +553,55 @@ var MemoryStorageBackend = class {
437
553
  generateSessionId() {
438
554
  return generateSessionId();
439
555
  }
440
- async create(session, ttl) {
556
+ async create(session) {
441
557
  const { sessionId, userId } = session;
442
558
  if (!sessionId || !userId) throw new Error("userId and sessionId required");
443
559
  const sessionKey = this.getSessionKey(userId, sessionId);
444
560
  if (this.sessions.has(sessionKey)) {
445
561
  throw new Error(`Session ${sessionId} already exists`);
446
562
  }
447
- this.sessions.set(sessionKey, session);
563
+ this.sessions.set(sessionKey, normalizeNewSession(session));
448
564
  if (!this.userIdSessions.has(userId)) {
449
565
  this.userIdSessions.set(userId, /* @__PURE__ */ new Set());
450
566
  }
451
567
  this.userIdSessions.get(userId).add(sessionId);
452
568
  }
453
- async update(userId, sessionId, data, ttl) {
569
+ async update(userId, sessionId, data) {
454
570
  if (!userId || !sessionId) throw new Error("userId and sessionId required");
455
571
  const sessionKey = this.getSessionKey(userId, sessionId);
456
572
  const current = this.sessions.get(sessionKey);
457
573
  if (!current) {
458
574
  throw new Error(`Session ${sessionId} not found`);
459
575
  }
460
- const updated = {
461
- ...current,
462
- ...data
463
- };
576
+ const updated = mergeSessionUpdate(current, data);
464
577
  this.sessions.set(sessionKey, updated);
465
578
  }
579
+ async patchCredentials(userId, sessionId, data) {
580
+ const sessionKey = this.getSessionKey(userId, sessionId);
581
+ if (!this.sessions.has(sessionKey)) {
582
+ throw new Error(`Session ${sessionId} not found`);
583
+ }
584
+ const current = this.credentials.get(sessionKey) ?? { sessionId, userId };
585
+ this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
586
+ }
466
587
  async get(userId, sessionId) {
467
588
  const sessionKey = this.getSessionKey(userId, sessionId);
468
589
  return this.sessions.get(sessionKey) || null;
469
590
  }
591
+ async getCredentials(userId, sessionId) {
592
+ const sessionKey = this.getSessionKey(userId, sessionId);
593
+ if (!this.sessions.has(sessionKey)) return null;
594
+ return this.credentials.get(sessionKey) ?? { sessionId, userId };
595
+ }
596
+ async clearCredentials(userId, sessionId) {
597
+ await this.patchCredentials(userId, sessionId, {
598
+ clientInformation: null,
599
+ tokens: null,
600
+ codeVerifier: null,
601
+ clientId: null,
602
+ oauthState: null
603
+ });
604
+ }
470
605
  async listIds(userId) {
471
606
  const set = this.userIdSessions.get(userId);
472
607
  return set ? Array.from(set) : [];
@@ -486,6 +621,7 @@ var MemoryStorageBackend = class {
486
621
  async delete(userId, sessionId) {
487
622
  const sessionKey = this.getSessionKey(userId, sessionId);
488
623
  this.sessions.delete(sessionKey);
624
+ this.credentials.delete(sessionKey);
489
625
  const set = this.userIdSessions.get(userId);
490
626
  if (set) {
491
627
  set.delete(sessionId);
@@ -499,9 +635,22 @@ var MemoryStorageBackend = class {
499
635
  }
500
636
  async clearAll() {
501
637
  this.sessions.clear();
638
+ this.credentials.clear();
502
639
  this.userIdSessions.clear();
503
640
  }
504
641
  async cleanupExpired() {
642
+ for (const [key, session] of this.sessions.entries()) {
643
+ if (!isSessionExpired(session)) continue;
644
+ this.sessions.delete(key);
645
+ this.credentials.delete(key);
646
+ const set = this.userIdSessions.get(session.userId);
647
+ if (set) {
648
+ set.delete(session.sessionId);
649
+ if (set.size === 0) {
650
+ this.userIdSessions.delete(session.userId);
651
+ }
652
+ }
653
+ }
505
654
  }
506
655
  async disconnect() {
507
656
  }
@@ -516,6 +665,7 @@ var FileStorageBackend = class {
516
665
  constructor(options = {}) {
517
666
  __publicField(this, "filePath");
518
667
  __publicField(this, "memoryCache", null);
668
+ __publicField(this, "credentialsCache", null);
519
669
  __publicField(this, "initialized", false);
520
670
  this.filePath = options.path || "./sessions.json";
521
671
  }
@@ -530,14 +680,22 @@ var FileStorageBackend = class {
530
680
  const data = await fs2.promises.readFile(this.filePath, "utf-8");
531
681
  const json = JSON.parse(data);
532
682
  this.memoryCache = /* @__PURE__ */ new Map();
533
- if (Array.isArray(json)) {
534
- json.forEach((s) => {
535
- this.memoryCache.set(this.getSessionKey(s.userId || "unknown", s.sessionId), s);
683
+ this.credentialsCache = /* @__PURE__ */ new Map();
684
+ if (Array.isArray(json.sessions)) {
685
+ json.sessions.forEach((s) => {
686
+ const session = normalizeStoredSession(s);
687
+ this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
688
+ });
689
+ }
690
+ if (Array.isArray(json.credentials)) {
691
+ json.credentials.forEach((c) => {
692
+ this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
536
693
  });
537
694
  }
538
695
  } catch (error) {
539
696
  if (error.code === "ENOENT") {
540
697
  this.memoryCache = /* @__PURE__ */ new Map();
698
+ this.credentialsCache = /* @__PURE__ */ new Map();
541
699
  await this.flush();
542
700
  } else {
543
701
  console.error("[FileStorage] Failed to load sessions:", error);
@@ -551,9 +709,11 @@ var FileStorageBackend = class {
551
709
  if (!this.initialized) await this.init();
552
710
  }
553
711
  async flush() {
554
- if (!this.memoryCache) return;
555
- const sessions2 = Array.from(this.memoryCache.values());
556
- await fs2.promises.writeFile(this.filePath, JSON.stringify(sessions2, null, 2), "utf-8");
712
+ if (!this.memoryCache || !this.credentialsCache) return;
713
+ await fs2.promises.writeFile(this.filePath, JSON.stringify({
714
+ sessions: Array.from(this.memoryCache.values()),
715
+ credentials: Array.from(this.credentialsCache.values())
716
+ }, null, 2), "utf-8");
557
717
  }
558
718
  getSessionKey(userId, sessionId) {
559
719
  return `${userId}:${sessionId}`;
@@ -561,7 +721,7 @@ var FileStorageBackend = class {
561
721
  generateSessionId() {
562
722
  return generateSessionId();
563
723
  }
564
- async create(session, ttl) {
724
+ async create(session) {
565
725
  await this.ensureInitialized();
566
726
  const { sessionId, userId } = session;
567
727
  if (!sessionId || !userId) throw new Error("userId and sessionId required");
@@ -569,10 +729,10 @@ var FileStorageBackend = class {
569
729
  if (this.memoryCache.has(sessionKey)) {
570
730
  throw new Error(`Session ${sessionId} already exists`);
571
731
  }
572
- this.memoryCache.set(sessionKey, session);
732
+ this.memoryCache.set(sessionKey, normalizeNewSession(session));
573
733
  await this.flush();
574
734
  }
575
- async update(userId, sessionId, data, ttl) {
735
+ async update(userId, sessionId, data) {
576
736
  await this.ensureInitialized();
577
737
  if (!userId || !sessionId) throw new Error("userId and sessionId required");
578
738
  const sessionKey = this.getSessionKey(userId, sessionId);
@@ -580,18 +740,40 @@ var FileStorageBackend = class {
580
740
  if (!current) {
581
741
  throw new Error(`Session ${sessionId} not found`);
582
742
  }
583
- const updated = {
584
- ...current,
585
- ...data
586
- };
743
+ const updated = mergeSessionUpdate(current, data);
587
744
  this.memoryCache.set(sessionKey, updated);
588
745
  await this.flush();
589
746
  }
747
+ async patchCredentials(userId, sessionId, data) {
748
+ await this.ensureInitialized();
749
+ const sessionKey = this.getSessionKey(userId, sessionId);
750
+ if (!this.memoryCache.has(sessionKey)) {
751
+ throw new Error(`Session ${sessionId} not found`);
752
+ }
753
+ const current = this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
754
+ this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
755
+ await this.flush();
756
+ }
590
757
  async get(userId, sessionId) {
591
758
  await this.ensureInitialized();
592
759
  const sessionKey = this.getSessionKey(userId, sessionId);
593
760
  return this.memoryCache.get(sessionKey) || null;
594
761
  }
762
+ async getCredentials(userId, sessionId) {
763
+ await this.ensureInitialized();
764
+ const sessionKey = this.getSessionKey(userId, sessionId);
765
+ if (!this.memoryCache.has(sessionKey)) return null;
766
+ return this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
767
+ }
768
+ async clearCredentials(userId, sessionId) {
769
+ await this.patchCredentials(userId, sessionId, {
770
+ clientInformation: null,
771
+ tokens: null,
772
+ codeVerifier: null,
773
+ clientId: null,
774
+ oauthState: null
775
+ });
776
+ }
595
777
  async list(userId) {
596
778
  await this.ensureInitialized();
597
779
  return Array.from(this.memoryCache.values()).filter((s) => s.userId === userId);
@@ -603,7 +785,9 @@ var FileStorageBackend = class {
603
785
  async delete(userId, sessionId) {
604
786
  await this.ensureInitialized();
605
787
  const sessionKey = this.getSessionKey(userId, sessionId);
606
- if (this.memoryCache.delete(sessionKey)) {
788
+ const deleted = this.memoryCache.delete(sessionKey);
789
+ this.credentialsCache.delete(sessionKey);
790
+ if (deleted) {
607
791
  await this.flush();
608
792
  }
609
793
  }
@@ -614,10 +798,21 @@ var FileStorageBackend = class {
614
798
  async clearAll() {
615
799
  await this.ensureInitialized();
616
800
  this.memoryCache.clear();
801
+ this.credentialsCache.clear();
617
802
  await this.flush();
618
803
  }
619
804
  async cleanupExpired() {
620
805
  await this.ensureInitialized();
806
+ let changed = false;
807
+ for (const [key, session] of this.memoryCache.entries()) {
808
+ if (!isSessionExpired(session)) continue;
809
+ this.memoryCache.delete(key);
810
+ this.credentialsCache.delete(key);
811
+ changed = true;
812
+ }
813
+ if (changed) {
814
+ await this.flush();
815
+ }
621
816
  }
622
817
  async disconnect() {
623
818
  }
@@ -629,10 +824,12 @@ var SqliteStorage = class {
629
824
  constructor(options = {}) {
630
825
  __publicField(this, "db", null);
631
826
  __publicField(this, "table");
827
+ __publicField(this, "credentialsTable");
632
828
  __publicField(this, "initialized", false);
633
829
  __publicField(this, "dbPath");
634
830
  this.dbPath = options.path || "./sessions.db";
635
831
  this.table = options.table || "mcp_sessions";
832
+ this.credentialsTable = `${this.table}_credentials`;
636
833
  }
637
834
  async init() {
638
835
  if (this.initialized) return;
@@ -643,6 +840,7 @@ var SqliteStorage = class {
643
840
  fs2__namespace.mkdirSync(dir, { recursive: true });
644
841
  }
645
842
  this.db = new DatabaseConstructor(this.dbPath);
843
+ this.db.pragma("foreign_keys = ON");
646
844
  this.db.exec(`
647
845
  CREATE TABLE IF NOT EXISTS ${this.table} (
648
846
  sessionId TEXT PRIMARY KEY,
@@ -651,6 +849,13 @@ var SqliteStorage = class {
651
849
  expiresAt INTEGER
652
850
  );
653
851
  CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
852
+ CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
853
+ sessionId TEXT NOT NULL,
854
+ userId TEXT NOT NULL,
855
+ data TEXT NOT NULL,
856
+ PRIMARY KEY (userId, sessionId),
857
+ FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
858
+ );
654
859
  `);
655
860
  this.initialized = true;
656
861
  console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
@@ -671,18 +876,18 @@ var SqliteStorage = class {
671
876
  generateSessionId() {
672
877
  return generateSessionId();
673
878
  }
674
- async create(session, ttl) {
879
+ async create(session) {
675
880
  this.ensureInitialized();
676
881
  const { sessionId, userId } = session;
677
882
  if (!sessionId || !userId) {
678
883
  throw new Error("userId and sessionId required");
679
884
  }
680
- const expiresAt = ttl ? Date.now() + ttl * 1e3 : null;
885
+ const sessionWithLifecycle = normalizeNewSession(session);
681
886
  try {
682
887
  const stmt = this.db.prepare(
683
888
  `INSERT INTO ${this.table} (sessionId, userId, data, expiresAt) VALUES (?, ?, ?, ?)`
684
889
  );
685
- stmt.run(sessionId, userId, JSON.stringify(session), expiresAt);
890
+ stmt.run(sessionId, userId, JSON.stringify(sessionWithLifecycle), sessionWithLifecycle.expiresAt ?? null);
686
891
  } catch (error) {
687
892
  if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
688
893
  throw new Error(`Session ${sessionId} already exists`);
@@ -690,7 +895,7 @@ var SqliteStorage = class {
690
895
  throw error;
691
896
  }
692
897
  }
693
- async update(userId, sessionId, data, ttl) {
898
+ async update(userId, sessionId, data) {
694
899
  this.ensureInitialized();
695
900
  if (!sessionId || !userId) {
696
901
  throw new Error("userId and sessionId required");
@@ -699,12 +904,25 @@ var SqliteStorage = class {
699
904
  if (!currentSession) {
700
905
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
701
906
  }
702
- const updatedSession = { ...currentSession, ...data };
703
- const expiresAt = ttl ? Date.now() + ttl * 1e3 : null;
907
+ const updatedSession = mergeSessionUpdate(currentSession, data);
704
908
  const stmt = this.db.prepare(
705
909
  `UPDATE ${this.table} SET data = ?, expiresAt = ? WHERE sessionId = ? AND userId = ?`
706
910
  );
707
- stmt.run(JSON.stringify(updatedSession), expiresAt, sessionId, userId);
911
+ stmt.run(JSON.stringify(updatedSession), updatedSession.expiresAt ?? null, sessionId, userId);
912
+ }
913
+ async patchCredentials(userId, sessionId, data) {
914
+ this.ensureInitialized();
915
+ if (!await this.get(userId, sessionId)) {
916
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
917
+ }
918
+ const current = await this.getCredentials(userId, sessionId) ?? { sessionId, userId };
919
+ const credentials = { ...current, ...data, sessionId, userId };
920
+ const stmt = this.db.prepare(
921
+ `INSERT INTO ${this.credentialsTable} (sessionId, userId, data)
922
+ VALUES (?, ?, ?)
923
+ ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
924
+ );
925
+ stmt.run(sessionId, userId, JSON.stringify(credentials));
708
926
  }
709
927
  async get(userId, sessionId) {
710
928
  this.ensureInitialized();
@@ -713,7 +931,25 @@ var SqliteStorage = class {
713
931
  );
714
932
  const row = stmt.get(sessionId, userId);
715
933
  if (!row) return null;
716
- return JSON.parse(row.data);
934
+ return normalizeStoredSession(JSON.parse(row.data));
935
+ }
936
+ async getCredentials(userId, sessionId) {
937
+ this.ensureInitialized();
938
+ if (!await this.get(userId, sessionId)) return null;
939
+ const stmt = this.db.prepare(
940
+ `SELECT data FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
941
+ );
942
+ const row = stmt.get(sessionId, userId);
943
+ return row ? JSON.parse(row.data) : { sessionId, userId };
944
+ }
945
+ async clearCredentials(userId, sessionId) {
946
+ await this.patchCredentials(userId, sessionId, {
947
+ clientInformation: null,
948
+ tokens: null,
949
+ codeVerifier: null,
950
+ clientId: null,
951
+ oauthState: null
952
+ });
717
953
  }
718
954
  async list(userId) {
719
955
  this.ensureInitialized();
@@ -721,7 +957,7 @@ var SqliteStorage = class {
721
957
  `SELECT data FROM ${this.table} WHERE userId = ?`
722
958
  );
723
959
  const rows = stmt.all(userId);
724
- return rows.map((row) => JSON.parse(row.data));
960
+ return rows.map((row) => normalizeStoredSession(JSON.parse(row.data)));
725
961
  }
726
962
  async listIds(userId) {
727
963
  this.ensureInitialized();
@@ -736,6 +972,9 @@ var SqliteStorage = class {
736
972
  const stmt = this.db.prepare(
737
973
  `DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
738
974
  );
975
+ this.db.prepare(
976
+ `DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
977
+ ).run(sessionId, userId);
739
978
  stmt.run(sessionId, userId);
740
979
  }
741
980
  async listAllIds() {
@@ -746,16 +985,28 @@ var SqliteStorage = class {
746
985
  }
747
986
  async clearAll() {
748
987
  this.ensureInitialized();
988
+ this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
749
989
  const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
750
990
  stmt.run();
751
991
  }
752
992
  async cleanupExpired() {
753
993
  this.ensureInitialized();
754
- const now = Date.now();
755
- const stmt = this.db.prepare(
756
- `DELETE FROM ${this.table} WHERE expiresAt IS NOT NULL AND expiresAt < ?`
757
- );
758
- stmt.run(now);
994
+ const rows = this.db.prepare(`SELECT sessionId, userId, data FROM ${this.table}`).all();
995
+ const deleteStmt = this.db.prepare(`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`);
996
+ for (const row of rows) {
997
+ const session = normalizeStoredSession(JSON.parse(row.data));
998
+ if (isSessionExpired(session)) {
999
+ deleteStmt.run(row.sessionId, row.userId);
1000
+ }
1001
+ }
1002
+ this.db.prepare(
1003
+ `DELETE FROM ${this.credentialsTable}
1004
+ WHERE NOT EXISTS (
1005
+ SELECT 1 FROM ${this.table}
1006
+ WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
1007
+ AND ${this.table}.userId = ${this.credentialsTable}.userId
1008
+ )`
1009
+ ).run();
759
1010
  }
760
1011
  async disconnect() {
761
1012
  if (this.db) {
@@ -840,19 +1091,21 @@ function decryptObject(data) {
840
1091
  var SupabaseStorageBackend = class {
841
1092
  constructor(supabase) {
842
1093
  this.supabase = supabase;
843
- __publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
844
1094
  }
845
1095
  async init() {
846
- const { error } = await this.supabase.from("mcp_sessions").select("session_id").limit(0);
847
- if (error) {
848
- if (error.code === "42P01") {
849
- throw new Error(
850
- '[SupabaseStorage] Table "mcp_sessions" not found in your database. Please run "npx mcp-ts supabase-init" in your project to set up the required table and RLS policies.'
851
- );
852
- }
853
- throw new Error(`[SupabaseStorage] Initialization check failed: ${error.message}`);
1096
+ await this.assertTable("mcp_sessions", "session_id");
1097
+ await this.assertTable("mcp_credentials", "session_id");
1098
+ console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
1099
+ }
1100
+ async assertTable(table, column) {
1101
+ const { error } = await this.supabase.from(table).select(column).limit(0);
1102
+ if (!error) return;
1103
+ if (error.code === "42P01") {
1104
+ throw new Error(
1105
+ `[SupabaseStorage] Table "${table}" not found in your database. Please run "npx mcp-ts supabase-init" to set up the required storage schema.`
1106
+ );
854
1107
  }
855
- console.log('[mcp-ts][Storage] Supabase: \u2713 "mcp_sessions" table verified.');
1108
+ throw new Error(`[SupabaseStorage] Initialization check failed for "${table}": ${error.message}`);
856
1109
  }
857
1110
  generateSessionId() {
858
1111
  return generateSessionId();
@@ -866,37 +1119,49 @@ var SupabaseStorageBackend = class {
866
1119
  transportType: row.transport_type,
867
1120
  callbackUrl: row.callback_url,
868
1121
  createdAt: new Date(row.created_at).getTime(),
1122
+ updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
1123
+ expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
869
1124
  userId: row.user_id,
870
1125
  headers: decryptObject(row.headers),
871
- active: row.active,
872
- clientInformation: row.client_information,
873
- tokens: decryptObject(row.tokens),
874
- codeVerifier: row.code_verifier,
875
- clientId: row.client_id
1126
+ authUrl: row.auth_url,
1127
+ status: row.status ?? "pending"
876
1128
  };
877
1129
  }
878
- async create(session, ttl) {
1130
+ mapRowToCredentials(row, userId, sessionId) {
1131
+ return {
1132
+ sessionId,
1133
+ userId,
1134
+ clientInformation: decryptObject(row?.client_information),
1135
+ tokens: decryptObject(row?.tokens),
1136
+ codeVerifier: decryptObject(row?.code_verifier),
1137
+ clientId: row?.client_id,
1138
+ oauthState: row?.oauth_state
1139
+ };
1140
+ }
1141
+ hasCredentialData(data) {
1142
+ return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
1143
+ }
1144
+ async create(session) {
879
1145
  const { sessionId, userId } = session;
880
1146
  if (!sessionId || !userId) throw new Error("userId and sessionId required");
881
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
882
- const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
1147
+ const status = session.status ?? "pending";
1148
+ const createdAt = new Date(session.createdAt || Date.now()).toISOString();
1149
+ const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
1150
+ const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
883
1151
  const { error } = await this.supabase.from("mcp_sessions").insert({
884
1152
  session_id: sessionId,
885
1153
  user_id: userId,
886
- // Maps user_id to userId to support RLS using auth.uid()
887
1154
  server_id: session.serverId,
888
1155
  server_name: session.serverName,
889
1156
  server_url: session.serverUrl,
890
1157
  transport_type: session.transportType,
891
1158
  callback_url: session.callbackUrl,
892
- created_at: new Date(session.createdAt || Date.now()).toISOString(),
1159
+ created_at: createdAt,
1160
+ updated_at: updatedAt,
893
1161
  headers: encryptObject(session.headers),
894
- active: session.active ?? false,
895
- client_information: session.clientInformation,
896
- tokens: encryptObject(session.tokens),
897
- code_verifier: session.codeVerifier,
898
- client_id: session.clientId,
899
- expires_at: expiresAt
1162
+ auth_url: session.authUrl ?? null,
1163
+ status,
1164
+ expires_at: expiresAt === null ? null : new Date(expiresAt).toISOString()
900
1165
  });
901
1166
  if (error) {
902
1167
  if (error.code === "23505") {
@@ -905,11 +1170,8 @@ var SupabaseStorageBackend = class {
905
1170
  throw new Error(`Failed to create session in Supabase: ${error.message}`);
906
1171
  }
907
1172
  }
908
- async update(userId, sessionId, data, ttl) {
909
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
910
- const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
1173
+ async update(userId, sessionId, data) {
911
1174
  const updateData = {
912
- expires_at: expiresAt,
913
1175
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
914
1176
  };
915
1177
  if ("serverId" in data) updateData.server_id = data.serverId;
@@ -917,20 +1179,50 @@ var SupabaseStorageBackend = class {
917
1179
  if ("serverUrl" in data) updateData.server_url = data.serverUrl;
918
1180
  if ("transportType" in data) updateData.transport_type = data.transportType;
919
1181
  if ("callbackUrl" in data) updateData.callback_url = data.callbackUrl;
920
- if ("active" in data) updateData.active = data.active;
1182
+ if ("status" in data) {
1183
+ const status = data.status ?? "pending";
1184
+ const expiresAt = resolveSessionExpiresAt(status);
1185
+ updateData.status = status;
1186
+ updateData.expires_at = expiresAt === null ? null : new Date(expiresAt).toISOString();
1187
+ }
921
1188
  if ("headers" in data) updateData.headers = encryptObject(data.headers);
922
- if ("clientInformation" in data) updateData.client_information = data.clientInformation;
923
- if ("tokens" in data) updateData.tokens = data.tokens === void 0 ? null : encryptObject(data.tokens);
924
- if ("codeVerifier" in data) updateData.code_verifier = data.codeVerifier;
925
- if ("clientId" in data) updateData.client_id = data.clientId;
926
- const { data: updatedRows, error } = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId).select("id");
927
- if (error) {
928
- throw new Error(`Failed to update session: ${error.message}`);
1189
+ if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
1190
+ const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
1191
+ let updatedRows = null;
1192
+ if (shouldUpdateSession) {
1193
+ const result = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId).select("id");
1194
+ if (result.error) {
1195
+ throw new Error(`Failed to update session: ${result.error.message}`);
1196
+ }
1197
+ updatedRows = result.data;
1198
+ } else {
1199
+ const result = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
1200
+ if (result.error) {
1201
+ throw new Error(`Failed to update session: ${result.error.message}`);
1202
+ }
1203
+ updatedRows = result.data;
929
1204
  }
930
1205
  if (!updatedRows || updatedRows.length === 0) {
931
1206
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
932
1207
  }
933
1208
  }
1209
+ async patchCredentials(userId, sessionId, data) {
1210
+ if (!this.hasCredentialData(data)) return;
1211
+ const row = {
1212
+ user_id: userId,
1213
+ session_id: sessionId,
1214
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1215
+ };
1216
+ if ("clientInformation" in data) row.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
1217
+ if ("tokens" in data) row.tokens = data.tokens == null ? null : encryptObject(data.tokens);
1218
+ if ("codeVerifier" in data) row.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
1219
+ if ("clientId" in data) row.client_id = data.clientId ?? null;
1220
+ if ("oauthState" in data) row.oauth_state = data.oauthState ?? null;
1221
+ const { error } = await this.supabase.from("mcp_credentials").upsert(row, { onConflict: "user_id,session_id" });
1222
+ if (error) {
1223
+ throw new Error(`Failed to update credentials: ${error.message}`);
1224
+ }
1225
+ }
934
1226
  async get(userId, sessionId) {
935
1227
  const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
936
1228
  if (error) {
@@ -940,6 +1232,21 @@ var SupabaseStorageBackend = class {
940
1232
  if (!data) return null;
941
1233
  return this.mapRowToSessionData(data);
942
1234
  }
1235
+ async getCredentials(userId, sessionId) {
1236
+ const { data, error } = await this.supabase.from("mcp_credentials").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
1237
+ if (error) {
1238
+ console.error("[SupabaseStorage] Failed to get credentials:", error);
1239
+ return null;
1240
+ }
1241
+ if (data) {
1242
+ return this.mapRowToCredentials(data, userId, sessionId);
1243
+ }
1244
+ const { data: sessionRows, error: sessionError } = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
1245
+ if (sessionError || !sessionRows || sessionRows.length === 0) {
1246
+ return null;
1247
+ }
1248
+ return { sessionId, userId };
1249
+ }
943
1250
  async list(userId) {
944
1251
  const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
945
1252
  if (error) {
@@ -948,6 +1255,12 @@ var SupabaseStorageBackend = class {
948
1255
  }
949
1256
  return data.map((row) => this.mapRowToSessionData(row));
950
1257
  }
1258
+ async clearCredentials(userId, sessionId) {
1259
+ const { error } = await this.supabase.from("mcp_credentials").delete().eq("user_id", userId).eq("session_id", sessionId);
1260
+ if (error) {
1261
+ throw new Error(`Failed to clear credentials: ${error.message}`);
1262
+ }
1263
+ }
951
1264
  async delete(userId, sessionId) {
952
1265
  const { error } = await this.supabase.from("mcp_sessions").delete().eq("user_id", userId).eq("session_id", sessionId);
953
1266
  if (error) {
@@ -971,15 +1284,24 @@ var SupabaseStorageBackend = class {
971
1284
  return data.map((row) => row.session_id);
972
1285
  }
973
1286
  async clearAll() {
1287
+ const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
1288
+ if (credentialsError) {
1289
+ console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
1290
+ }
974
1291
  const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
975
1292
  if (error) {
976
1293
  console.error("[SupabaseStorage] Failed to clear sessions:", error);
977
1294
  }
978
1295
  }
979
1296
  async cleanupExpired() {
980
- const { error } = await this.supabase.from("mcp_sessions").delete().lt("expires_at", (/* @__PURE__ */ new Date()).toISOString());
981
- if (error) {
982
- console.error("[SupabaseStorage] Failed to cleanup expired sessions:", error);
1297
+ const { error: transientError } = await this.supabase.from("mcp_sessions").delete().not("expires_at", "is", null).neq("status", "active").lt("expires_at", (/* @__PURE__ */ new Date()).toISOString());
1298
+ if (transientError) {
1299
+ console.error("[SupabaseStorage] Failed to cleanup expired inactive sessions:", transientError);
1300
+ }
1301
+ const dormantCutoff = new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString();
1302
+ const { error: dormantError } = await this.supabase.from("mcp_sessions").delete().eq("status", "active").lt("updated_at", dormantCutoff);
1303
+ if (dormantError) {
1304
+ console.error("[SupabaseStorage] Failed to cleanup dormant active sessions:", dormantError);
983
1305
  }
984
1306
  }
985
1307
  async disconnect() {
@@ -991,23 +1313,29 @@ init_cjs_shims();
991
1313
  var NeonStorageBackend = class {
992
1314
  constructor(sql, options = {}) {
993
1315
  this.sql = sql;
994
- __publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
995
1316
  __publicField(this, "tableName");
1317
+ __publicField(this, "credentialsTableName");
996
1318
  const schema = options.schema || "public";
997
1319
  const table = options.table || "mcp_sessions";
1320
+ const credentialsTable = options.credentialsTable || "mcp_credentials";
998
1321
  this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
1322
+ this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
999
1323
  }
1000
1324
  async init() {
1325
+ await this.assertTable(this.tableName, "mcp_sessions");
1326
+ await this.assertTable(this.credentialsTableName, "mcp_credentials");
1327
+ console.log("[mcp-ts][Storage] Neon: storage tables verified.");
1328
+ }
1329
+ async assertTable(qualifiedName, displayName) {
1001
1330
  const [{ exists } = { exists: null }] = await this.sql.query(
1002
1331
  "SELECT to_regclass($1) AS exists",
1003
- [this.tableName.replace(/"/g, "")]
1332
+ [qualifiedName.replace(/"/g, "")]
1004
1333
  );
1005
1334
  if (!exists) {
1006
1335
  throw new Error(
1007
- '[NeonStorage] Table "mcp_sessions" not found in your database. Please create it using the Neon storage guide in docs/storage-backends/neon.md.'
1336
+ `[NeonStorage] Table "${displayName}" not found in your database. Please create it using the Neon storage guide in docs/storage-backends/neon.md.`
1008
1337
  );
1009
1338
  }
1010
- console.log('[mcp-ts][Storage] Neon: "mcp_sessions" table verified.');
1011
1339
  }
1012
1340
  generateSessionId() {
1013
1341
  return generateSessionId();
@@ -1027,20 +1355,35 @@ var NeonStorageBackend = class {
1027
1355
  transportType: row.transport_type,
1028
1356
  callbackUrl: row.callback_url,
1029
1357
  createdAt: new Date(row.created_at).getTime(),
1358
+ updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
1359
+ expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
1030
1360
  userId: row.user_id,
1031
1361
  headers: decryptObject(row.headers),
1032
- active: row.active ?? false,
1033
- clientInformation: row.client_information,
1362
+ authUrl: row.auth_url ?? void 0,
1363
+ status: row.status ?? "pending"
1364
+ };
1365
+ }
1366
+ mapRowToCredentials(row, userId, sessionId) {
1367
+ return {
1368
+ sessionId,
1369
+ userId,
1370
+ clientInformation: decryptObject(row.client_information),
1034
1371
  tokens: decryptObject(row.tokens),
1035
- codeVerifier: row.code_verifier ?? void 0,
1036
- clientId: row.client_id ?? void 0
1372
+ codeVerifier: decryptObject(row.code_verifier),
1373
+ clientId: row.client_id ?? void 0,
1374
+ oauthState: row.oauth_state
1037
1375
  };
1038
1376
  }
1039
- async create(session, ttl) {
1377
+ hasCredentialData(data) {
1378
+ return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
1379
+ }
1380
+ async create(session) {
1040
1381
  const { sessionId, userId } = session;
1041
1382
  if (!sessionId || !userId) throw new Error("userId and sessionId required");
1042
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
1043
- const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
1383
+ const status = session.status ?? "pending";
1384
+ const createdAt = new Date(session.createdAt || Date.now()).toISOString();
1385
+ const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
1386
+ const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
1044
1387
  try {
1045
1388
  await this.sql.query(
1046
1389
  `INSERT INTO ${this.tableName} (
@@ -1052,16 +1395,14 @@ var NeonStorageBackend = class {
1052
1395
  transport_type,
1053
1396
  callback_url,
1054
1397
  created_at,
1398
+ updated_at,
1055
1399
  headers,
1056
- active,
1057
- client_information,
1058
- tokens,
1059
- code_verifier,
1060
- client_id,
1400
+ auth_url,
1401
+ status,
1061
1402
  expires_at
1062
1403
  ) VALUES (
1063
1404
  $1, $2, $3, $4, $5, $6, $7, $8,
1064
- $9, $10, $11, $12, $13, $14, $15
1405
+ $9, $10, $11, $12, $13
1065
1406
  )`,
1066
1407
  [
1067
1408
  sessionId,
@@ -1071,14 +1412,12 @@ var NeonStorageBackend = class {
1071
1412
  session.serverUrl,
1072
1413
  session.transportType,
1073
1414
  session.callbackUrl,
1074
- new Date(session.createdAt || Date.now()).toISOString(),
1415
+ createdAt,
1416
+ updatedAt,
1075
1417
  encryptObject(session.headers),
1076
- session.active ?? false,
1077
- session.clientInformation,
1078
- encryptObject(session.tokens),
1079
- session.codeVerifier,
1080
- session.clientId,
1081
- expiresAt
1418
+ session.authUrl ?? null,
1419
+ status,
1420
+ expiresAt === null ? null : new Date(expiresAt).toISOString()
1082
1421
  ]
1083
1422
  );
1084
1423
  } catch (error) {
@@ -1088,52 +1427,86 @@ var NeonStorageBackend = class {
1088
1427
  throw new Error(`Failed to create session in Neon: ${error.message}`);
1089
1428
  }
1090
1429
  }
1091
- async update(userId, sessionId, data, ttl) {
1430
+ async update(userId, sessionId, data) {
1092
1431
  const currentSession = await this.get(userId, sessionId);
1093
1432
  if (!currentSession) {
1094
1433
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
1095
1434
  }
1096
1435
  const updatedSession = { ...currentSession, ...data };
1097
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
1098
- const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
1099
- const updatedRows = await this.sql.query(
1100
- `UPDATE ${this.tableName}
1101
- SET
1102
- server_id = $1,
1103
- server_name = $2,
1104
- server_url = $3,
1105
- transport_type = $4,
1106
- callback_url = $5,
1107
- active = $6,
1108
- headers = $7,
1109
- client_information = $8,
1110
- tokens = $9,
1111
- code_verifier = $10,
1112
- client_id = $11,
1113
- expires_at = $12,
1114
- updated_at = now()
1115
- WHERE user_id = $13 AND session_id = $14
1116
- RETURNING id`,
1436
+ const status = updatedSession.status ?? "pending";
1437
+ const expiresAt = resolveSessionExpiresAt(status);
1438
+ const shouldUpdateSession = "serverId" in data || "serverName" in data || "serverUrl" in data || "transportType" in data || "callbackUrl" in data || "status" in data || "headers" in data || "authUrl" in data;
1439
+ if (shouldUpdateSession) {
1440
+ const updatedRows = await this.sql.query(
1441
+ `UPDATE ${this.tableName}
1442
+ SET
1443
+ server_id = $1,
1444
+ server_name = $2,
1445
+ server_url = $3,
1446
+ transport_type = $4,
1447
+ callback_url = $5,
1448
+ status = $6,
1449
+ headers = $7,
1450
+ auth_url = $8,
1451
+ expires_at = $9,
1452
+ updated_at = now()
1453
+ WHERE user_id = $10 AND session_id = $11
1454
+ RETURNING id`,
1455
+ [
1456
+ updatedSession.serverId,
1457
+ updatedSession.serverName,
1458
+ updatedSession.serverUrl,
1459
+ updatedSession.transportType,
1460
+ updatedSession.callbackUrl,
1461
+ status,
1462
+ encryptObject(updatedSession.headers),
1463
+ updatedSession.authUrl ?? null,
1464
+ expiresAt === null ? null : new Date(expiresAt).toISOString(),
1465
+ userId,
1466
+ sessionId
1467
+ ]
1468
+ );
1469
+ if (updatedRows.length === 0) {
1470
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
1471
+ }
1472
+ }
1473
+ }
1474
+ async patchCredentials(userId, sessionId, data) {
1475
+ if (!this.hasCredentialData(data)) return;
1476
+ await this.sql.query(
1477
+ `INSERT INTO ${this.credentialsTableName} (
1478
+ user_id,
1479
+ session_id,
1480
+ client_information,
1481
+ tokens,
1482
+ code_verifier,
1483
+ client_id,
1484
+ oauth_state,
1485
+ updated_at
1486
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, now())
1487
+ ON CONFLICT (user_id, session_id)
1488
+ DO UPDATE SET
1489
+ client_information = CASE WHEN $8 THEN EXCLUDED.client_information ELSE ${this.credentialsTableName}.client_information END,
1490
+ tokens = CASE WHEN $9 THEN EXCLUDED.tokens ELSE ${this.credentialsTableName}.tokens END,
1491
+ code_verifier = CASE WHEN $10 THEN EXCLUDED.code_verifier ELSE ${this.credentialsTableName}.code_verifier END,
1492
+ client_id = CASE WHEN $11 THEN EXCLUDED.client_id ELSE ${this.credentialsTableName}.client_id END,
1493
+ oauth_state = CASE WHEN $12 THEN EXCLUDED.oauth_state ELSE ${this.credentialsTableName}.oauth_state END,
1494
+ updated_at = now()`,
1117
1495
  [
1118
- updatedSession.serverId,
1119
- updatedSession.serverName,
1120
- updatedSession.serverUrl,
1121
- updatedSession.transportType,
1122
- updatedSession.callbackUrl,
1123
- updatedSession.active ?? false,
1124
- encryptObject(updatedSession.headers),
1125
- updatedSession.clientInformation,
1126
- updatedSession.tokens === void 0 ? null : encryptObject(updatedSession.tokens),
1127
- updatedSession.codeVerifier,
1128
- updatedSession.clientId,
1129
- expiresAt,
1130
1496
  userId,
1131
- sessionId
1497
+ sessionId,
1498
+ "clientInformation" in data ? data.clientInformation == null ? null : encryptObject(data.clientInformation) : null,
1499
+ "tokens" in data ? data.tokens == null ? null : encryptObject(data.tokens) : null,
1500
+ "codeVerifier" in data ? data.codeVerifier == null ? null : encryptObject(data.codeVerifier) : null,
1501
+ "clientId" in data ? data.clientId ?? null : null,
1502
+ "oauthState" in data ? data.oauthState ?? null : null,
1503
+ "clientInformation" in data,
1504
+ "tokens" in data,
1505
+ "codeVerifier" in data,
1506
+ "clientId" in data,
1507
+ "oauthState" in data
1132
1508
  ]
1133
1509
  );
1134
- if (updatedRows.length === 0) {
1135
- throw new Error(`Session ${sessionId} not found for userId ${userId}`);
1136
- }
1137
1510
  }
1138
1511
  async get(userId, sessionId) {
1139
1512
  try {
@@ -1141,12 +1514,32 @@ var NeonStorageBackend = class {
1141
1514
  `SELECT * FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1142
1515
  [userId, sessionId]
1143
1516
  );
1144
- return rows[0] ? this.mapRowToSessionData(rows[0]) : null;
1517
+ if (!rows[0]) return null;
1518
+ return this.mapRowToSessionData(rows[0]);
1145
1519
  } catch (error) {
1146
1520
  console.error("[NeonStorage] Failed to get session:", error);
1147
1521
  return null;
1148
1522
  }
1149
1523
  }
1524
+ async getCredentials(userId, sessionId) {
1525
+ try {
1526
+ const credentialRows = await this.sql.query(
1527
+ `SELECT * FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
1528
+ [userId, sessionId]
1529
+ );
1530
+ if (credentialRows[0]) {
1531
+ return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
1532
+ }
1533
+ const sessionRows = await this.sql.query(
1534
+ `SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1535
+ [userId, sessionId]
1536
+ );
1537
+ return sessionRows[0] ? { sessionId, userId } : null;
1538
+ } catch (error) {
1539
+ console.error("[NeonStorage] Failed to get credentials:", error);
1540
+ return null;
1541
+ }
1542
+ }
1150
1543
  async list(userId) {
1151
1544
  try {
1152
1545
  const rows = await this.sql.query(
@@ -1159,6 +1552,16 @@ var NeonStorageBackend = class {
1159
1552
  return [];
1160
1553
  }
1161
1554
  }
1555
+ async clearCredentials(userId, sessionId) {
1556
+ try {
1557
+ await this.sql.query(
1558
+ `DELETE FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
1559
+ [userId, sessionId]
1560
+ );
1561
+ } catch (error) {
1562
+ console.error("[NeonStorage] Failed to clear credentials:", error);
1563
+ }
1564
+ }
1162
1565
  async delete(userId, sessionId) {
1163
1566
  try {
1164
1567
  await this.sql.query(
@@ -1194,6 +1597,7 @@ var NeonStorageBackend = class {
1194
1597
  }
1195
1598
  async clearAll() {
1196
1599
  try {
1600
+ await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
1197
1601
  await this.sql.query(`DELETE FROM ${this.tableName}`);
1198
1602
  } catch (error) {
1199
1603
  console.error("[NeonStorage] Failed to clear sessions:", error);
@@ -1202,9 +1606,17 @@ var NeonStorageBackend = class {
1202
1606
  async cleanupExpired() {
1203
1607
  try {
1204
1608
  await this.sql.query(
1205
- `DELETE FROM ${this.tableName} WHERE expires_at < $1`,
1609
+ `DELETE FROM ${this.tableName}
1610
+ WHERE expires_at IS NOT NULL
1611
+ AND expires_at < $1
1612
+ AND status <> 'active'`,
1206
1613
  [(/* @__PURE__ */ new Date()).toISOString()]
1207
1614
  );
1615
+ await this.sql.query(
1616
+ `DELETE FROM ${this.tableName}
1617
+ WHERE status = 'active' AND updated_at < $1`,
1618
+ [new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString()]
1619
+ );
1208
1620
  } catch (error) {
1209
1621
  console.error("[NeonStorage] Failed to cleanup expired sessions:", error);
1210
1622
  }
@@ -1254,26 +1666,24 @@ function emitSessionMutation(event) {
1254
1666
  function createSessionMutationEvent(prop, args) {
1255
1667
  const timestamp = Date.now();
1256
1668
  if (prop === "create") {
1257
- const [session, ttl] = args;
1669
+ const [session] = args;
1258
1670
  if (!session?.userId || !session?.sessionId) return null;
1259
1671
  return {
1260
1672
  type: "create",
1261
1673
  userId: session.userId,
1262
1674
  sessionId: session.sessionId,
1263
1675
  session,
1264
- ttl,
1265
1676
  timestamp
1266
1677
  };
1267
1678
  }
1268
1679
  if (prop === "update") {
1269
- const [userId, sessionId, patch, ttl] = args;
1680
+ const [userId, sessionId, patch] = args;
1270
1681
  if (!userId || !sessionId) return null;
1271
1682
  return {
1272
1683
  type: "update",
1273
1684
  userId,
1274
1685
  sessionId,
1275
1686
  patch,
1276
- ttl,
1277
1687
  timestamp
1278
1688
  };
1279
1689
  }
@@ -1469,7 +1879,6 @@ var StorageOAuthClientProvider = class {
1469
1879
  __publicField(this, "_authUrl");
1470
1880
  __publicField(this, "_clientId");
1471
1881
  __publicField(this, "onRedirectCallback");
1472
- __publicField(this, "tokenExpiresAt");
1473
1882
  this.userId = options.userId;
1474
1883
  this.serverId = options.serverId;
1475
1884
  this.sessionId = options.sessionId;
@@ -1503,30 +1912,30 @@ var StorageOAuthClientProvider = class {
1503
1912
  this._clientId = clientId_;
1504
1913
  }
1505
1914
  /**
1506
- * Loads OAuth data from the session store
1915
+ * Loads OAuth credentials from the session store
1507
1916
  * @private
1508
1917
  */
1509
- async getSessionData() {
1510
- const data = await sessions.get(this.userId, this.sessionId);
1918
+ async getCredentials() {
1919
+ const data = await sessions.getCredentials(this.userId, this.sessionId);
1511
1920
  if (!data) {
1512
- return {};
1921
+ return { userId: this.userId, sessionId: this.sessionId };
1513
1922
  }
1514
1923
  return data;
1515
1924
  }
1516
1925
  /**
1517
- * Saves OAuth data to the session store
1518
- * @param data - Partial OAuth data to save
1926
+ * Saves OAuth credentials to the session store
1927
+ * @param data - Partial OAuth credentials to save
1519
1928
  * @private
1520
1929
  * @throws Error if session doesn't exist (session must be created by controller layer)
1521
1930
  */
1522
- async saveSessionData(data) {
1523
- await sessions.update(this.userId, this.sessionId, data);
1931
+ async patchCredentials(data) {
1932
+ await sessions.patchCredentials(this.userId, this.sessionId, data);
1524
1933
  }
1525
1934
  /**
1526
1935
  * Retrieves stored OAuth client information
1527
1936
  */
1528
1937
  async clientInformation() {
1529
- const data = await this.getSessionData();
1938
+ const data = await this.getCredentials();
1530
1939
  if (data.clientId && !this._clientId) {
1531
1940
  this._clientId = data.clientId;
1532
1941
  }
@@ -1545,7 +1954,7 @@ var StorageOAuthClientProvider = class {
1545
1954
  * Stores OAuth client information
1546
1955
  */
1547
1956
  async saveClientInformation(clientInformation) {
1548
- await this.saveSessionData({
1957
+ await this.patchCredentials({
1549
1958
  clientInformation,
1550
1959
  clientId: clientInformation.client_id
1551
1960
  });
@@ -1555,29 +1964,58 @@ var StorageOAuthClientProvider = class {
1555
1964
  * Stores OAuth tokens
1556
1965
  */
1557
1966
  async saveTokens(tokens) {
1558
- const data = { tokens };
1559
- if (tokens.expires_in) {
1560
- this.tokenExpiresAt = Date.now() + tokens.expires_in * 1e3 - TOKEN_EXPIRY_BUFFER_MS;
1561
- }
1562
- await this.saveSessionData(data);
1967
+ await this.patchCredentials({ tokens });
1563
1968
  }
1564
1969
  get authUrl() {
1565
1970
  return this._authUrl;
1566
1971
  }
1567
1972
  async state() {
1568
- return this.sessionId;
1973
+ const nonce = nanoid.nanoid(32);
1974
+ await this.patchCredentials({
1975
+ oauthState: {
1976
+ nonce,
1977
+ sessionId: this.sessionId,
1978
+ serverId: this.serverId,
1979
+ createdAt: Date.now()
1980
+ },
1981
+ codeVerifier: null
1982
+ });
1983
+ return formatOAuthState(nonce, this.sessionId);
1569
1984
  }
1570
- async checkState(_state) {
1571
- const data = await sessions.get(this.userId, this.sessionId);
1985
+ async checkState(state) {
1986
+ const parsed = parseOAuthState(state);
1987
+ if (!parsed) {
1988
+ return { valid: false, error: "Invalid OAuth state" };
1989
+ }
1990
+ if (parsed.sessionId !== this.sessionId) {
1991
+ return { valid: false, error: "OAuth state mismatch" };
1992
+ }
1993
+ const data = await sessions.getCredentials(this.userId, parsed.sessionId);
1572
1994
  if (!data) {
1573
1995
  return { valid: false, error: "Session not found" };
1574
1996
  }
1575
- return { valid: true, serverId: this.serverId };
1997
+ const oauthState = data.oauthState;
1998
+ if (!oauthState) {
1999
+ return { valid: false, error: "OAuth state not found" };
2000
+ }
2001
+ if (oauthState.nonce !== parsed.nonce || oauthState.sessionId !== parsed.sessionId || oauthState.serverId !== this.serverId) {
2002
+ return { valid: false, error: "OAuth state mismatch" };
2003
+ }
2004
+ if (Date.now() - oauthState.createdAt > STATE_EXPIRATION_MS) {
2005
+ return { valid: false, error: "OAuth state expired" };
2006
+ }
2007
+ return { valid: true, serverId: oauthState.serverId };
1576
2008
  }
1577
- async consumeState(_state) {
2009
+ async consumeState(state) {
2010
+ const result = await this.checkState(state);
2011
+ if (!result.valid) {
2012
+ throw new Error(result.error || "Invalid OAuth state");
2013
+ }
2014
+ await this.patchCredentials({ oauthState: null });
1578
2015
  }
1579
2016
  async redirectToAuthorization(authUrl) {
1580
2017
  this._authUrl = authUrl.toString();
2018
+ await sessions.update(this.userId, this.sessionId, { authUrl: this._authUrl });
1581
2019
  if (this.onRedirectCallback) {
1582
2020
  this.onRedirectCallback(authUrl.toString());
1583
2021
  }
@@ -1588,21 +2026,25 @@ var StorageOAuthClientProvider = class {
1588
2026
  } else {
1589
2027
  const updates = {};
1590
2028
  if (scope === "client") {
1591
- updates.clientInformation = void 0;
1592
- updates.clientId = void 0;
2029
+ updates.clientInformation = null;
2030
+ updates.clientId = null;
1593
2031
  } else if (scope === "tokens") {
1594
- updates.tokens = void 0;
2032
+ updates.tokens = null;
1595
2033
  } else if (scope === "verifier") {
1596
- updates.codeVerifier = void 0;
2034
+ updates.codeVerifier = null;
1597
2035
  }
1598
- await this.saveSessionData(updates);
2036
+ await this.patchCredentials(updates);
1599
2037
  }
1600
2038
  }
1601
2039
  async saveCodeVerifier(verifier) {
1602
- await this.saveSessionData({ codeVerifier: verifier });
2040
+ const data = await this.getCredentials();
2041
+ if (data.codeVerifier) {
2042
+ return;
2043
+ }
2044
+ await this.patchCredentials({ codeVerifier: verifier });
1603
2045
  }
1604
2046
  async codeVerifier() {
1605
- const data = await this.getSessionData();
2047
+ const data = await this.getCredentials();
1606
2048
  if (data.clientId && !this._clientId) {
1607
2049
  this._clientId = data.clientId;
1608
2050
  }
@@ -1612,23 +2054,14 @@ var StorageOAuthClientProvider = class {
1612
2054
  return data.codeVerifier;
1613
2055
  }
1614
2056
  async deleteCodeVerifier() {
1615
- await this.saveSessionData({ codeVerifier: void 0 });
2057
+ await this.patchCredentials({ codeVerifier: null });
1616
2058
  }
1617
2059
  async tokens() {
1618
- const data = await this.getSessionData();
2060
+ const data = await this.getCredentials();
1619
2061
  if (data.clientId && !this._clientId) {
1620
2062
  this._clientId = data.clientId;
1621
2063
  }
1622
- return data.tokens;
1623
- }
1624
- isTokenExpired() {
1625
- if (!this.tokenExpiresAt) {
1626
- return false;
1627
- }
1628
- return Date.now() >= this.tokenExpiresAt;
1629
- }
1630
- setTokenExpiresAt(expiresAt) {
1631
- this.tokenExpiresAt = expiresAt;
2064
+ return data.tokens ?? void 0;
1632
2065
  }
1633
2066
  };
1634
2067
 
@@ -1855,7 +2288,7 @@ var MCPClient = class {
1855
2288
  * Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
1856
2289
  * This wrapper enforces a timeout and properly uses AbortController to unblock the request.
1857
2290
  */
1858
- fetch: (url, init) => {
2291
+ fetch: async (url, init) => {
1859
2292
  const timeout = 3e4;
1860
2293
  const controller = new AbortController();
1861
2294
  const timeoutId = setTimeout(() => controller.abort(), timeout);
@@ -1863,7 +2296,17 @@ var MCPClient = class {
1863
2296
  // @ts-ignore: AbortSignal.any is available in Node 20+
1864
2297
  AbortSignal.any ? AbortSignal.any([init.signal, controller.signal]) : controller.signal
1865
2298
  ) : controller.signal;
1866
- 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
+ }
1867
2310
  }
1868
2311
  };
1869
2312
  if (type === "sse") {
@@ -1884,17 +2327,18 @@ var MCPClient = class {
1884
2327
  }
1885
2328
  this.emitStateChange("INITIALIZING");
1886
2329
  this.emitProgress("Loading session configuration...");
2330
+ let existingSession = null;
1887
2331
  if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
1888
- const sessionData = await sessions.get(this.userId, this.sessionId);
1889
- if (!sessionData) {
2332
+ existingSession = await sessions.get(this.userId, this.sessionId);
2333
+ if (!existingSession) {
1890
2334
  throw new Error(`Session not found: ${this.sessionId}`);
1891
2335
  }
1892
- this.serverUrl = this.serverUrl || sessionData.serverUrl;
1893
- this.callbackUrl = this.callbackUrl || sessionData.callbackUrl;
1894
- this.serverName = this.serverName || sessionData.serverName;
1895
- this.serverId = this.serverId || sessionData.serverId || "unknown";
1896
- this.headers = this.headers || sessionData.headers;
1897
- this.createdAt = sessionData.createdAt;
2336
+ this.serverUrl = this.serverUrl || existingSession.serverUrl;
2337
+ this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
2338
+ this.serverName = this.serverName || existingSession.serverName;
2339
+ this.serverId = this.serverId || existingSession.serverId || "unknown";
2340
+ this.headers = this.headers || existingSession.headers;
2341
+ this.createdAt = existingSession.createdAt;
1898
2342
  }
1899
2343
  if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
1900
2344
  throw new Error("Missing required connection metadata");
@@ -1938,10 +2382,13 @@ var MCPClient = class {
1938
2382
  }
1939
2383
  );
1940
2384
  }
1941
- const existingSession = await sessions.get(this.userId, this.sessionId);
2385
+ if (existingSession === null) {
2386
+ existingSession = await sessions.get(this.userId, this.sessionId);
2387
+ }
1942
2388
  if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
1943
2389
  this.createdAt = Date.now();
1944
- console.log(`[MCPClient] Creating initial session ${this.sessionId} for OAuth flow`);
2390
+ const updatedAt = this.createdAt;
2391
+ console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
1945
2392
  await sessions.create({
1946
2393
  sessionId: this.sessionId,
1947
2394
  userId: this.userId,
@@ -1952,18 +2399,18 @@ var MCPClient = class {
1952
2399
  transportType: this.transportType || "streamable-http",
1953
2400
  headers: this.headers,
1954
2401
  createdAt: this.createdAt,
1955
- active: false
1956
- }, Math.floor(STATE_EXPIRATION_MS / 1e3));
2402
+ updatedAt,
2403
+ status: "pending"
2404
+ });
1957
2405
  }
1958
2406
  }
1959
2407
  /**
1960
2408
  * Saves current session state to the session store
1961
2409
  * Creates new session if it doesn't exist, updates if it does
1962
- * @param ttl - Time-to-live in seconds (defaults to 12hr for connected sessions)
1963
- * @param active - Session status marker used to avoid unnecessary TTL rewrites
2410
+ * @param status - Session lifecycle status used by storage cleanup
1964
2411
  * @private
1965
2412
  */
1966
- async saveSession(ttl = SESSION_TTL_SECONDS, active = true) {
2413
+ async saveSession(status = "active", existingSession) {
1967
2414
  if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
1968
2415
  return;
1969
2416
  }
@@ -1977,13 +2424,29 @@ var MCPClient = class {
1977
2424
  transportType: this.transportType || "streamable-http",
1978
2425
  headers: this.headers,
1979
2426
  createdAt: this.createdAt || Date.now(),
1980
- active
2427
+ updatedAt: Date.now(),
2428
+ status
1981
2429
  };
1982
- const existingSession = await sessions.get(this.userId, this.sessionId);
2430
+ if (status === "active") {
2431
+ sessionData.authUrl = null;
2432
+ }
2433
+ if (existingSession === void 0) {
2434
+ existingSession = await sessions.get(this.userId, this.sessionId);
2435
+ }
1983
2436
  if (existingSession) {
1984
- await sessions.update(this.userId, this.sessionId, sessionData, ttl);
2437
+ await sessions.update(this.userId, this.sessionId, sessionData);
1985
2438
  } else {
1986
- await sessions.create(sessionData, ttl);
2439
+ await sessions.create(sessionData);
2440
+ }
2441
+ }
2442
+ /**
2443
+ * Removes transient setup/auth sessions without masking the original error.
2444
+ * @private
2445
+ */
2446
+ async deleteTransientSession() {
2447
+ try {
2448
+ await sessions.delete(this.userId, this.sessionId);
2449
+ } catch {
1987
2450
  }
1988
2451
  }
1989
2452
  /**
@@ -2035,6 +2498,14 @@ var MCPClient = class {
2035
2498
  * @throws {Error} When connection fails for other reasons
2036
2499
  */
2037
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
+ }
2038
2509
  await this.initialize();
2039
2510
  if (!this.client || !this.oauthProvider) {
2040
2511
  const error = "Client or OAuth provider not initialized";
@@ -2048,8 +2519,8 @@ var MCPClient = class {
2048
2519
  this.transportType = transportType;
2049
2520
  this.emitStateChange("CONNECTED");
2050
2521
  this.emitProgress("Connected successfully");
2051
- console.log(`[MCPClient] Saving session ${this.sessionId} with 12hr TTL (connect success)`);
2052
- await this.saveSession(SESSION_TTL_SECONDS, true);
2522
+ console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
2523
+ await this.saveSession("active");
2053
2524
  } catch (error) {
2054
2525
  if (error instanceof auth_js.UnauthorizedError || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
2055
2526
  let authUrl = "";
@@ -2061,15 +2532,12 @@ var MCPClient = class {
2061
2532
  const message = detail.toLowerCase() === "unauthorized" ? "OAuth authorization URL not available" : `OAuth authorization URL not available: ${detail}`;
2062
2533
  this.emitError(message, "auth");
2063
2534
  this.emitStateChange("FAILED");
2064
- try {
2065
- await sessions.delete(this.userId, this.sessionId);
2066
- } catch {
2067
- }
2535
+ await this.deleteTransientSession();
2068
2536
  throw new Error(message);
2069
2537
  }
2070
2538
  this.emitStateChange("AUTHENTICATING");
2071
- console.log(`[MCPClient] Saving session ${this.sessionId} with 10min TTL (OAuth pending)`);
2072
- await this.saveSession(Math.floor(STATE_EXPIRATION_MS / 1e3), false);
2539
+ console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
2540
+ await this.saveSession("pending");
2073
2541
  if (this.serverId) {
2074
2542
  this._onConnectionEvent.fire({
2075
2543
  type: "auth_required",
@@ -2089,7 +2557,7 @@ var MCPClient = class {
2089
2557
  this.emitStateChange("FAILED");
2090
2558
  try {
2091
2559
  const existingSession = await sessions.get(this.userId, this.sessionId);
2092
- if (!existingSession || existingSession.active !== true) {
2560
+ if (!existingSession || existingSession.status !== "active") {
2093
2561
  await sessions.delete(this.userId, this.sessionId);
2094
2562
  }
2095
2563
  } catch {
@@ -2104,7 +2572,7 @@ var MCPClient = class {
2104
2572
  * @param authCode - Authorization code received from OAuth callback
2105
2573
  */
2106
2574
  // TODO: needs to be optimized
2107
- async finishAuth(authCode) {
2575
+ async finishAuth(authCode, state) {
2108
2576
  this.emitStateChange("AUTHENTICATING");
2109
2577
  this.emitProgress("Exchanging authorization code for tokens...");
2110
2578
  await this.initialize();
@@ -2114,6 +2582,16 @@ var MCPClient = class {
2114
2582
  this.emitStateChange("FAILED");
2115
2583
  throw new Error(error);
2116
2584
  }
2585
+ if (state) {
2586
+ const stateCheck = await this.oauthProvider.checkState(state);
2587
+ if (!stateCheck.valid) {
2588
+ const error = stateCheck.error || "Invalid OAuth state";
2589
+ this.emitError(error, "auth");
2590
+ this.emitStateChange("FAILED");
2591
+ throw new Error(error);
2592
+ }
2593
+ await this.oauthProvider.consumeState(state);
2594
+ }
2117
2595
  const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
2118
2596
  let lastError;
2119
2597
  let tokensExchanged = false;
@@ -2153,8 +2631,8 @@ var MCPClient = class {
2153
2631
  await this.client.connect(this.transport);
2154
2632
  this.transportType = currentType;
2155
2633
  this.emitStateChange("CONNECTED");
2156
- console.log(`[MCPClient] Updating session ${this.sessionId} to 12hr TTL (OAuth complete)`);
2157
- await this.saveSession(SESSION_TTL_SECONDS, true);
2634
+ console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
2635
+ await this.saveSession("active");
2158
2636
  return;
2159
2637
  } catch (error) {
2160
2638
  lastError = error;
@@ -2167,12 +2645,14 @@ var MCPClient = class {
2167
2645
  const msg = error instanceof Error ? error.message : "Authentication failed";
2168
2646
  this.emitError(msg, "auth");
2169
2647
  this.emitStateChange("FAILED");
2648
+ await this.deleteTransientSession();
2170
2649
  throw error;
2171
2650
  }
2172
2651
  if (isLastAttempt) {
2173
2652
  const msg = error instanceof Error ? error.message : "Authentication failed";
2174
2653
  this.emitError(msg, "auth");
2175
2654
  this.emitStateChange("FAILED");
2655
+ await this.deleteTransientSession();
2176
2656
  throw error;
2177
2657
  }
2178
2658
  this.emitProgress(`Auth attempt with ${currentType} failed: ${errorMessage}. Retrying...`);
@@ -2182,6 +2662,7 @@ var MCPClient = class {
2182
2662
  const errorMessage = lastError instanceof Error ? lastError.message : "Authentication failed";
2183
2663
  this.emitError(errorMessage, "auth");
2184
2664
  this.emitStateChange("FAILED");
2665
+ await this.deleteTransientSession();
2185
2666
  throw lastError;
2186
2667
  }
2187
2668
  }
@@ -2403,7 +2884,7 @@ var MCPClient = class {
2403
2884
  await this.oauthProvider.invalidateCredentials("all");
2404
2885
  }
2405
2886
  await sessions.delete(this.userId, this.sessionId);
2406
- this.disconnect();
2887
+ await this.disconnect();
2407
2888
  }
2408
2889
  /**
2409
2890
  * Checks if the client is currently connected to an MCP server
@@ -2413,10 +2894,21 @@ var MCPClient = class {
2413
2894
  return this.client !== null;
2414
2895
  }
2415
2896
  /**
2416
- * Disconnects from the MCP server and cleans up resources
2417
- * 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.
2418
2904
  */
2419
- disconnect(reason) {
2905
+ async disconnect() {
2906
+ if (this.transport instanceof streamableHttp_js.StreamableHTTPClientTransport) {
2907
+ try {
2908
+ await this.transport.terminateSession();
2909
+ } catch {
2910
+ }
2911
+ }
2420
2912
  if (this.client) {
2421
2913
  this.client.close();
2422
2914
  }
@@ -2428,7 +2920,6 @@ var MCPClient = class {
2428
2920
  type: "disconnected",
2429
2921
  sessionId: this.sessionId,
2430
2922
  serverId: this.serverId,
2431
- reason,
2432
2923
  timestamp: Date.now()
2433
2924
  });
2434
2925
  this._onObservabilityEvent.fire({
@@ -2437,9 +2928,7 @@ var MCPClient = class {
2437
2928
  message: `Disconnected from ${this.serverId}`,
2438
2929
  sessionId: this.sessionId,
2439
2930
  serverId: this.serverId,
2440
- payload: {
2441
- reason: reason || "unknown"
2442
- },
2931
+ payload: {},
2443
2932
  timestamp: Date.now(),
2444
2933
  id: nanoid.nanoid()
2445
2934
  });
@@ -2496,48 +2985,6 @@ var MCPClient = class {
2496
2985
  getSessionId() {
2497
2986
  return this.sessionId;
2498
2987
  }
2499
- /**
2500
- * Gets MCP server configuration for all active user sessions
2501
- * Loads sessions from storage and returns server connection metadata.
2502
- * OAuth refresh is handled by SDK transports through their authProvider.
2503
- * @deprecated This returns legacy connection metadata only and does not
2504
- * include OAuth tokens or generated Authorization headers. Prefer
2505
- * MultiSessionClient or explicit MCPClient instances so SDK transports can
2506
- * own OAuth refresh and reauthorization.
2507
- * @param userId - User ID to fetch sessions for
2508
- * @returns Object keyed by sanitized server labels containing transport and url.
2509
- * @static
2510
- */
2511
- static async getMcpServerConfig(userId) {
2512
- const mcpConfig = {};
2513
- const sessionList = await sessions.list(userId);
2514
- await Promise.all(
2515
- sessionList.map(async (sessionData) => {
2516
- const { sessionId } = sessionData;
2517
- try {
2518
- if (!sessionData.serverId || !sessionData.transportType || !sessionData.serverUrl || !sessionData.callbackUrl) {
2519
- await sessions.delete(userId, sessionId);
2520
- return;
2521
- }
2522
- const label = sanitizeServerLabel(
2523
- sessionData.serverName || sessionData.serverId || "server"
2524
- );
2525
- mcpConfig[label] = {
2526
- transport: sessionData.transportType,
2527
- url: sessionData.serverUrl,
2528
- ...sessionData.serverName && {
2529
- serverName: sessionData.serverName,
2530
- serverLabel: label
2531
- }
2532
- };
2533
- } catch (error) {
2534
- await sessions.delete(userId, sessionId);
2535
- console.warn(`[MCP] Failed to process session ${sessionId}:`, error);
2536
- }
2537
- })
2538
- );
2539
- return mcpConfig;
2540
- }
2541
2988
  };
2542
2989
 
2543
2990
  // src/server/mcp/multi-session-client.ts
@@ -2573,18 +3020,13 @@ var MultiSessionClient = class {
2573
3020
  *
2574
3021
  * A session is considered connectable when:
2575
3022
  * - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
2576
- * - Its `active` flag is not explicitly `false` sessions with `active: false` are
2577
- * either mid-OAuth flow, auth-pending, or previously failed. We skip those here
3023
+ * - Its status is `active`. Pending sessions are skipped here
2578
3024
  * and let the OAuth flow complete separately before we try to reconnect them.
2579
- *
2580
- * Note: Sessions where `active` is `undefined` (legacy records) are included
2581
- * for backwards compatibility.
2582
3025
  */
2583
3026
  async getActiveSessions() {
2584
3027
  const sessionList = await sessions.list(this.userId);
2585
3028
  const valid = sessionList.filter(
2586
- (s) => s.serverId && s.serverUrl && s.callbackUrl && s.active !== false
2587
- // exclude OAuth-pending / failed sessions
3029
+ (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
2588
3030
  );
2589
3031
  return valid;
2590
3032
  }
@@ -2605,6 +3047,9 @@ var MultiSessionClient = class {
2605
3047
  * Connects a single session, with built-in deduplication to prevent race conditions.
2606
3048
  *
2607
3049
  * - If a client for this session already exists and is connected, returns immediately.
3050
+ * - If the existing client entry is no longer connected (e.g. it was explicitly
3051
+ * disconnected), it is evicted so that `establishConnectionWithRetries` creates a
3052
+ * fresh transport — preventing "Client already connected" errors from the SDK.
2608
3053
  * - If a connection attempt for this session is already in-flight (e.g. from a
2609
3054
  * concurrent call), it joins the existing promise instead of starting a new one.
2610
3055
  * This is the key concurrency lock — the `connectionPromises` map acts as a
@@ -2612,9 +3057,12 @@ var MultiSessionClient = class {
2612
3057
  * - On completion (success or failure), the promise is cleaned up from the map.
2613
3058
  */
2614
3059
  async connectSession(session) {
2615
- const existingClient = this.clients.find((c) => c.getSessionId() === session.sessionId);
2616
- if (existingClient?.isConnected()) {
2617
- return;
3060
+ const existing = this.clients.find((c) => c.getSessionId() === session.sessionId);
3061
+ if (existing) {
3062
+ if (existing.isConnected()) {
3063
+ return;
3064
+ }
3065
+ this.clients = this.clients.filter((c) => c !== existing);
2618
3066
  }
2619
3067
  if (this.connectionPromises.has(session.sessionId)) {
2620
3068
  return this.connectionPromises.get(session.sessionId);
@@ -2689,8 +3137,24 @@ var MultiSessionClient = class {
2689
3137
  */
2690
3138
  async connect() {
2691
3139
  const sessions2 = await this.getActiveSessions();
3140
+ const activeSessionIds = new Set(sessions2.map((s) => s.sessionId));
3141
+ this.clients = this.clients.filter((c) => activeSessionIds.has(c.getSessionId()));
2692
3142
  await this.connectInBatches(sessions2);
2693
3143
  }
3144
+ /**
3145
+ * Drops all cached `MCPClient` instances and reconnects fresh from storage.
3146
+ *
3147
+ * Call this when downstream MCP servers have expired their transport sessions
3148
+ * (e.g. after a remote server restart) and subsequent tool calls return
3149
+ * "Session not found. Reconnect without session header." errors.
3150
+ *
3151
+ * OAuth tokens are preserved in the storage backend — no re-authentication
3152
+ * is required. Only the in-memory transport sessions are cleared.
3153
+ */
3154
+ async reconnect() {
3155
+ await this.disconnect();
3156
+ await this.connect();
3157
+ }
2694
3158
  /**
2695
3159
  * Returns all currently connected `MCPClient` instances.
2696
3160
  *
@@ -2703,11 +3167,15 @@ var MultiSessionClient = class {
2703
3167
  /**
2704
3168
  * Gracefully disconnects all active MCP clients and clears the internal client list.
2705
3169
  *
3170
+ * For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
3171
+ * endpoint per the spec before closing locally. All disconnects run in
3172
+ * parallel so shutdown is not serialised across many sessions.
3173
+ *
2706
3174
  * Call this during server shutdown or when a user logs out to free up
2707
3175
  * underlying transport resources (SSE streams, HTTP connections, etc.).
2708
3176
  */
2709
- disconnect() {
2710
- this.clients.forEach((client) => client.disconnect());
3177
+ async disconnect() {
3178
+ await Promise.all(this.clients.map((client) => client.disconnect()));
2711
3179
  this.clients = [];
2712
3180
  }
2713
3181
  };
@@ -2859,7 +3327,8 @@ var SSEConnectionManager = class {
2859
3327
  serverUrl: s.serverUrl,
2860
3328
  transport: s.transportType,
2861
3329
  createdAt: s.createdAt,
2862
- active: s.active !== false
3330
+ updatedAt: s.updatedAt ?? s.createdAt,
3331
+ status: s.status ?? "pending"
2863
3332
  }))
2864
3333
  };
2865
3334
  }
@@ -2875,7 +3344,7 @@ var SSEConnectionManager = class {
2875
3344
  (s) => s.serverId === serverId || s.serverUrl === serverUrl
2876
3345
  );
2877
3346
  if (duplicate) {
2878
- if (duplicate.active === false) {
3347
+ if (duplicate.status === "pending") {
2879
3348
  await this.getSession({ sessionId: duplicate.sessionId });
2880
3349
  return {
2881
3350
  sessionId: duplicate.sessionId,
@@ -2940,7 +3409,6 @@ var SSEConnectionManager = class {
2940
3409
  const client = this.clients.get(sessionId);
2941
3410
  if (client) {
2942
3411
  await client.clearSession();
2943
- client.disconnect();
2944
3412
  this.clients.delete(sessionId);
2945
3413
  } else {
2946
3414
  await sessions.delete(this.userId, sessionId);
@@ -3062,7 +3530,10 @@ var SSEConnectionManager = class {
3062
3530
  * Complete OAuth authorization flow
3063
3531
  */
3064
3532
  async finishAuth(params) {
3065
- const { sessionId, code } = params;
3533
+ const { code } = params;
3534
+ const oauthState = params.state;
3535
+ const parsedState = parseOAuthState(oauthState);
3536
+ const sessionId = parsedState?.sessionId || oauthState;
3066
3537
  const session = await sessions.get(this.userId, sessionId);
3067
3538
  if (!session) {
3068
3539
  throw new Error("Session not found");
@@ -3087,7 +3558,7 @@ var SSEConnectionManager = class {
3087
3558
  headers: session.headers
3088
3559
  });
3089
3560
  client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3090
- await client.finishAuth(code);
3561
+ await client.finishAuth(code, oauthState);
3091
3562
  this.clients.set(sessionId, client);
3092
3563
  const tools = await client.listTools();
3093
3564
  return { success: true, toolCount: tools.tools.length };
@@ -3146,14 +3617,14 @@ var SSEConnectionManager = class {
3146
3617
  /**
3147
3618
  * Cleanup and close all connections
3148
3619
  */
3149
- dispose() {
3620
+ async dispose() {
3150
3621
  this.isActive = false;
3151
3622
  if (this.heartbeatTimer) {
3152
3623
  clearInterval(this.heartbeatTimer);
3153
3624
  }
3154
- for (const client of this.clients.values()) {
3155
- client.disconnect();
3156
- }
3625
+ await Promise.all(
3626
+ Array.from(this.clients.values()).map((client) => client.disconnect())
3627
+ );
3157
3628
  this.clients.clear();
3158
3629
  }
3159
3630
  };