@mcp-ts/sdk 2.3.4 → 2.4.0

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 (82) hide show
  1. package/README.md +113 -30
  2. package/dist/adapters/agui-adapter.d.mts +3 -3
  3. package/dist/adapters/agui-adapter.d.ts +3 -3
  4. package/dist/adapters/agui-middleware.d.mts +3 -3
  5. package/dist/adapters/agui-middleware.d.ts +3 -3
  6. package/dist/adapters/ai-adapter.d.mts +3 -3
  7. package/dist/adapters/ai-adapter.d.ts +3 -3
  8. package/dist/adapters/langchain-adapter.d.mts +3 -3
  9. package/dist/adapters/langchain-adapter.d.ts +3 -3
  10. package/dist/adapters/mastra-adapter.d.mts +1 -1
  11. package/dist/adapters/mastra-adapter.d.ts +1 -1
  12. package/dist/client/index.d.mts +2 -2
  13. package/dist/client/index.d.ts +2 -2
  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 +9 -8
  19. package/dist/client/react.d.ts +9 -8
  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 +6 -5
  25. package/dist/client/vue.d.ts +6 -5
  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/{index-Cfjsme-a.d.mts → index-ByIjEReo.d.mts} +2 -2
  31. package/dist/{index-CmjMd2ac.d.ts → index-CtXvKl8N.d.ts} +2 -2
  32. package/dist/index.d.mts +5 -5
  33. package/dist/index.d.ts +5 -5
  34. package/dist/index.js +728 -306
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +722 -304
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{multi-session-client-C7hGqzgM.d.mts → multi-session-client-CIMUGF8S.d.mts} +15 -33
  39. package/dist/{multi-session-client-C_kPHpD5.d.ts → multi-session-client-CnvZEGPY.d.ts} +15 -33
  40. package/dist/server/index.d.mts +45 -17
  41. package/dist/server/index.d.ts +45 -17
  42. package/dist/server/index.js +720 -300
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +716 -299
  45. package/dist/server/index.mjs.map +1 -1
  46. package/dist/shared/index.d.mts +9 -8
  47. package/dist/shared/index.d.ts +9 -8
  48. package/dist/shared/index.js +7 -5
  49. package/dist/shared/index.js.map +1 -1
  50. package/dist/shared/index.mjs +5 -4
  51. package/dist/shared/index.mjs.map +1 -1
  52. package/dist/{tool-router-BMzhoNYt.d.ts → tool-router-CZMrOG8J.d.ts} +1 -1
  53. package/dist/{tool-router-BhHsvBfP.d.mts → tool-router-CuApsDiV.d.mts} +1 -1
  54. package/dist/{types-CxFaaZrM.d.mts → types-DCk_IF4L.d.mts} +5 -3
  55. package/dist/{types-CxFaaZrM.d.ts → types-DCk_IF4L.d.ts} +5 -3
  56. package/migrations/neon/20260513010000_install_mcp_sessions.sql +40 -3
  57. package/migrations/neon/20260513020000_add_session_cleanup_cron.sql +4 -4
  58. package/migrations/supabase/20260330195700_install_mcp_sessions.sql +67 -3
  59. package/migrations/supabase/20260421010000_add_session_cleanup_cron.sql +6 -6
  60. package/package.json +13 -3
  61. package/src/client/core/sse-client.ts +2 -2
  62. package/src/client/react/index.ts +1 -1
  63. package/src/client/react/oauth-popup.tsx +34 -13
  64. package/src/client/react/use-mcp.ts +19 -45
  65. package/src/client/utils/session-state.ts +43 -0
  66. package/src/client/vue/use-mcp.ts +18 -47
  67. package/src/server/handlers/sse-handler.ts +9 -4
  68. package/src/server/mcp/multi-session-client.ts +2 -6
  69. package/src/server/mcp/oauth-client.ts +73 -101
  70. package/src/server/mcp/storage-oauth-provider.ts +79 -50
  71. package/src/server/storage/file-backend.ts +74 -18
  72. package/src/server/storage/index.ts +2 -4
  73. package/src/server/storage/memory-backend.ts +49 -13
  74. package/src/server/storage/neon-backend.ts +201 -68
  75. package/src/server/storage/redis-backend.ts +81 -23
  76. package/src/server/storage/session-lifecycle.ts +78 -0
  77. package/src/server/storage/sqlite-backend.ts +89 -15
  78. package/src/server/storage/supabase-backend.ts +188 -63
  79. package/src/server/storage/types.ts +49 -16
  80. package/src/shared/constants.ts +5 -6
  81. package/src/shared/types.ts +5 -3
  82. package/src/shared/utils.ts +26 -0
package/dist/index.js CHANGED
@@ -160,24 +160,9 @@ init_cjs_shims();
160
160
  // src/server/storage/redis-backend.ts
161
161
  init_cjs_shims();
162
162
 
163
- // src/shared/constants.ts
164
- init_cjs_shims();
165
- var SESSION_TTL_SECONDS = 43200;
166
- var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
167
- var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
168
- var REDIS_KEY_PREFIX = "mcp:session:";
169
- var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
170
- var DEFAULT_CLIENT_NAME = "MCP Assistant";
171
- var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
172
- var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
173
- var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
174
- var SOFTWARE_ID = "@mcp-ts";
175
- var SOFTWARE_VERSION = "1.3.4";
176
- var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
177
- var MCP_CLIENT_VERSION = "2.0";
178
-
179
163
  // src/shared/utils.ts
180
164
  init_cjs_shims();
165
+ var OAUTH_STATE_SEPARATOR = ".";
181
166
  var firstChar = nanoid.customAlphabet(
182
167
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
183
168
  1
@@ -196,13 +181,102 @@ function sanitizeServerLabel(name) {
196
181
  function generateSessionId() {
197
182
  return firstChar() + rest();
198
183
  }
184
+ function formatOAuthState(nonce, sessionId) {
185
+ return `${nonce}${OAUTH_STATE_SEPARATOR}${sessionId}`;
186
+ }
187
+ function parseOAuthState(state) {
188
+ const separatorIndex = state.indexOf(OAUTH_STATE_SEPARATOR);
189
+ if (separatorIndex <= 0 || separatorIndex === state.length - 1) {
190
+ return void 0;
191
+ }
192
+ const nonce = state.slice(0, separatorIndex);
193
+ const sessionId = state.slice(separatorIndex + 1);
194
+ if (!nonce || !sessionId || sessionId.includes(OAUTH_STATE_SEPARATOR)) {
195
+ return void 0;
196
+ }
197
+ return { nonce, sessionId };
198
+ }
199
+
200
+ // src/server/storage/session-lifecycle.ts
201
+ init_cjs_shims();
202
+
203
+ // src/shared/constants.ts
204
+ init_cjs_shims();
205
+ var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
206
+ var PENDING_SESSION_EXPIRATION_SECONDS = Math.floor(STATE_EXPIRATION_MS / 1e3);
207
+ var DORMANT_SESSION_EXPIRATION_MS = 30 * 24 * 60 * 60 * 1e3;
208
+ var DORMANT_SESSION_EXPIRATION_SECONDS = Math.floor(DORMANT_SESSION_EXPIRATION_MS / 1e3);
209
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
210
+ var REDIS_KEY_PREFIX = "mcp:session:";
211
+ var DEFAULT_CLIENT_NAME = "MCP Assistant";
212
+ var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
213
+ var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
214
+ var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
215
+ var SOFTWARE_ID = "@mcp-ts";
216
+ var SOFTWARE_VERSION = "2.3.4";
217
+ var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
218
+ var MCP_CLIENT_VERSION = "2.0";
219
+
220
+ // src/server/storage/session-lifecycle.ts
221
+ function resolveSessionExpiresAt(status = "pending", referenceTime = Date.now()) {
222
+ return status === "active" ? null : referenceTime + STATE_EXPIRATION_MS;
223
+ }
224
+ function resolveSessionRedisTtlSeconds(session) {
225
+ return session.status === "active" ? DORMANT_SESSION_EXPIRATION_SECONDS : Math.floor(STATE_EXPIRATION_MS / 1e3);
226
+ }
227
+ function normalizeNewSession(session, now = Date.now()) {
228
+ const createdAt = session.createdAt || now;
229
+ const updatedAt = session.updatedAt ?? createdAt;
230
+ const status = session.status ?? "pending";
231
+ return {
232
+ ...session,
233
+ status,
234
+ createdAt,
235
+ updatedAt,
236
+ expiresAt: resolveSessionExpiresAt(status, status === "active" ? updatedAt : createdAt)
237
+ };
238
+ }
239
+ function mergeSessionUpdate(current, data, now = Date.now()) {
240
+ const updatedAt = data.updatedAt ?? now;
241
+ const updated = {
242
+ ...current,
243
+ ...data,
244
+ updatedAt
245
+ };
246
+ const status = updated.status ?? "pending";
247
+ return {
248
+ ...updated,
249
+ status,
250
+ expiresAt: resolveSessionExpiresAt(status, updatedAt)
251
+ };
252
+ }
253
+ function normalizeStoredSession(session) {
254
+ const createdAt = session.createdAt || Date.now();
255
+ const updatedAt = session.updatedAt ?? createdAt;
256
+ const status = session.status ?? "pending";
257
+ const expiresAt = status === "active" ? null : session.expiresAt ?? resolveSessionExpiresAt(status, createdAt);
258
+ return {
259
+ ...session,
260
+ status,
261
+ createdAt,
262
+ updatedAt,
263
+ expiresAt
264
+ };
265
+ }
266
+ function isSessionExpired(session, now = Date.now()) {
267
+ const hydrated = normalizeStoredSession(session);
268
+ if (hydrated.status === "active") {
269
+ return hydrated.updatedAt !== void 0 && hydrated.updatedAt < now - DORMANT_SESSION_EXPIRATION_MS;
270
+ }
271
+ return typeof hydrated.expiresAt === "number" && hydrated.expiresAt < now;
272
+ }
199
273
 
200
274
  // src/server/storage/redis-backend.ts
201
275
  var RedisStorageBackend = class {
202
276
  constructor(redis2) {
203
277
  this.redis = redis2;
204
- __publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
205
278
  __publicField(this, "KEY_PREFIX", "mcp:session:");
279
+ __publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
206
280
  __publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
207
281
  __publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
208
282
  }
@@ -221,6 +295,9 @@ var RedisStorageBackend = class {
221
295
  getSessionKey(userId, sessionId) {
222
296
  return `${this.KEY_PREFIX}${userId}:${sessionId}`;
223
297
  }
298
+ getCredentialsKey(userId, sessionId) {
299
+ return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
300
+ }
224
301
  /**
225
302
  * Generates Redis key for tracking all sessions for a user
226
303
  * @private
@@ -258,15 +335,16 @@ var RedisStorageBackend = class {
258
335
  generateSessionId() {
259
336
  return generateSessionId();
260
337
  }
261
- async create(session, ttl) {
338
+ async create(session) {
262
339
  const { sessionId, userId } = session;
263
340
  if (!sessionId || !userId) throw new Error("userId and sessionId required");
264
341
  const sessionKey = this.getSessionKey(userId, sessionId);
265
342
  const userIdKey = this.getUserIdKey(userId);
266
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
343
+ const sessionWithLifecycle = normalizeNewSession(session);
344
+ const effectiveTtl = resolveSessionRedisTtlSeconds(sessionWithLifecycle);
267
345
  const result = await this.redis.set(
268
346
  sessionKey,
269
- JSON.stringify(session),
347
+ JSON.stringify(sessionWithLifecycle),
270
348
  "EX",
271
349
  effectiveTtl,
272
350
  "NX"
@@ -276,9 +354,8 @@ var RedisStorageBackend = class {
276
354
  }
277
355
  await this.redis.sadd(userIdKey, sessionId);
278
356
  }
279
- async update(userId, sessionId, data, ttl) {
357
+ async update(userId, sessionId, data) {
280
358
  const sessionKey = this.getSessionKey(userId, sessionId);
281
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
282
359
  const script = `
283
360
  local currentStr = redis.call("GET", KEYS[1])
284
361
  if not currentStr then
@@ -286,26 +363,47 @@ var RedisStorageBackend = class {
286
363
  end
287
364
 
288
365
  local current = cjson.decode(currentStr)
289
- local updates = cjson.decode(ARGV[1])
290
-
291
- for k,v in pairs(updates) do
292
- current[k] = v
293
- end
366
+ local updated = cjson.decode(ARGV[1])
294
367
 
295
- redis.call("SET", KEYS[1], cjson.encode(current), "EX", ARGV[2])
368
+ redis.call("SET", KEYS[1], cjson.encode(updated), "EX", ARGV[2])
296
369
  return 1
297
370
  `;
371
+ const current = await this.get(userId, sessionId);
372
+ if (!current) {
373
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
374
+ }
375
+ const updated = mergeSessionUpdate(current, data);
376
+ const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
298
377
  const result = await this.redis.eval(
299
378
  script,
300
379
  1,
301
380
  sessionKey,
302
- JSON.stringify(data),
381
+ JSON.stringify(updated),
303
382
  effectiveTtl
304
383
  );
305
384
  if (result === 0) {
306
385
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
307
386
  }
308
387
  }
388
+ async patchCredentials(userId, sessionId, data) {
389
+ const sessionKey = this.getSessionKey(userId, sessionId);
390
+ const credentialsKey = this.getCredentialsKey(userId, sessionId);
391
+ const sessionExists = await this.redis.exists(sessionKey);
392
+ if (!sessionExists) {
393
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
394
+ }
395
+ const session = await this.get(userId, sessionId);
396
+ const currentTtl = await this.redis.ttl(sessionKey);
397
+ const effectiveTtl = currentTtl > 0 && session ? currentTtl : resolveSessionRedisTtlSeconds(session ?? { status: "pending" });
398
+ const currentStr = await this.redis.get(credentialsKey);
399
+ const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
400
+ const credentials = { ...current, ...data, sessionId, userId };
401
+ if (effectiveTtl > 0) {
402
+ await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
403
+ } else {
404
+ await this.redis.set(credentialsKey, JSON.stringify(credentials));
405
+ }
406
+ }
309
407
  async get(userId, sessionId) {
310
408
  try {
311
409
  const sessionKey = this.getSessionKey(userId, sessionId);
@@ -313,13 +411,28 @@ var RedisStorageBackend = class {
313
411
  if (!sessionDataStr) {
314
412
  return null;
315
413
  }
316
- const Session = JSON.parse(sessionDataStr);
317
- return Session;
414
+ const session = JSON.parse(sessionDataStr);
415
+ return normalizeStoredSession(session);
318
416
  } catch (error) {
319
417
  console.error("[RedisStorageBackend] Failed to get session:", error);
320
418
  return null;
321
419
  }
322
420
  }
421
+ async getCredentials(userId, sessionId) {
422
+ const session = await this.get(userId, sessionId);
423
+ if (!session) return null;
424
+ const credentialsStr = await this.redis.get(this.getCredentialsKey(userId, sessionId));
425
+ return credentialsStr ? JSON.parse(credentialsStr) : { sessionId, userId };
426
+ }
427
+ async clearCredentials(userId, sessionId) {
428
+ await this.patchCredentials(userId, sessionId, {
429
+ clientInformation: null,
430
+ tokens: null,
431
+ codeVerifier: null,
432
+ clientId: null,
433
+ oauthState: null
434
+ });
435
+ }
323
436
  async listIds(userId) {
324
437
  const sessions2 = await this.list(userId);
325
438
  return sessions2.map((session) => session.sessionId);
@@ -339,7 +452,7 @@ var RedisStorageBackend = class {
339
452
  if (staleSessionIds.length > 0) {
340
453
  await this.redis.srem(userIdKey, ...staleSessionIds);
341
454
  }
342
- return results.filter((session) => session !== null);
455
+ return results.filter((session) => session !== null).map((session) => normalizeStoredSession(session));
343
456
  } catch (error) {
344
457
  console.error(`[RedisStorageBackend] Failed to get session data for ${userId}:`, error);
345
458
  return [];
@@ -348,9 +461,10 @@ var RedisStorageBackend = class {
348
461
  async delete(userId, sessionId) {
349
462
  try {
350
463
  const sessionKey = this.getSessionKey(userId, sessionId);
464
+ const credentialsKey = this.getCredentialsKey(userId, sessionId);
351
465
  const userIdKey = this.getUserIdKey(userId);
352
466
  await this.redis.srem(userIdKey, sessionId);
353
- await this.redis.del(sessionKey);
467
+ await this.redis.del(sessionKey, credentialsKey);
354
468
  } catch (error) {
355
469
  console.error("[RedisStorageBackend] Failed to remove session:", error);
356
470
  }
@@ -365,7 +479,7 @@ var RedisStorageBackend = class {
365
479
  return null;
366
480
  }
367
481
  try {
368
- return JSON.parse(data).sessionId;
482
+ return normalizeStoredSession(JSON.parse(data)).sessionId;
369
483
  } catch (error) {
370
484
  console.error("[RedisStorageBackend] Failed to parse session while listing all session IDs:", error);
371
485
  return null;
@@ -381,8 +495,9 @@ var RedisStorageBackend = class {
381
495
  async clearAll() {
382
496
  try {
383
497
  const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
498
+ const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
384
499
  const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
385
- const allKeys = [...keys, ...userIdKeys];
500
+ const allKeys = [...keys, ...credentialKeys, ...userIdKeys];
386
501
  if (allKeys.length > 0) {
387
502
  await this.redis.del(...allKeys);
388
503
  }
@@ -406,6 +521,7 @@ var RedisStorageBackend = class {
406
521
  const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
407
522
  if (staleSessionIds.length > 0) {
408
523
  await this.redis.srem(userIdKey, ...staleSessionIds);
524
+ await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
409
525
  }
410
526
  const remainingCount = await this.redis.scard(userIdKey);
411
527
  if (remainingCount === 0) {
@@ -431,6 +547,7 @@ var MemoryStorageBackend = class {
431
547
  constructor() {
432
548
  // Map<userId:sessionId, Session>
433
549
  __publicField(this, "sessions", /* @__PURE__ */ new Map());
550
+ __publicField(this, "credentials", /* @__PURE__ */ new Map());
434
551
  // Map<userId, Set<sessionId>>
435
552
  __publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
436
553
  }
@@ -443,36 +560,55 @@ var MemoryStorageBackend = class {
443
560
  generateSessionId() {
444
561
  return generateSessionId();
445
562
  }
446
- async create(session, ttl) {
563
+ async create(session) {
447
564
  const { sessionId, userId } = session;
448
565
  if (!sessionId || !userId) throw new Error("userId and sessionId required");
449
566
  const sessionKey = this.getSessionKey(userId, sessionId);
450
567
  if (this.sessions.has(sessionKey)) {
451
568
  throw new Error(`Session ${sessionId} already exists`);
452
569
  }
453
- this.sessions.set(sessionKey, session);
570
+ this.sessions.set(sessionKey, normalizeNewSession(session));
454
571
  if (!this.userIdSessions.has(userId)) {
455
572
  this.userIdSessions.set(userId, /* @__PURE__ */ new Set());
456
573
  }
457
574
  this.userIdSessions.get(userId).add(sessionId);
458
575
  }
459
- async update(userId, sessionId, data, ttl) {
576
+ async update(userId, sessionId, data) {
460
577
  if (!userId || !sessionId) throw new Error("userId and sessionId required");
461
578
  const sessionKey = this.getSessionKey(userId, sessionId);
462
579
  const current = this.sessions.get(sessionKey);
463
580
  if (!current) {
464
581
  throw new Error(`Session ${sessionId} not found`);
465
582
  }
466
- const updated = {
467
- ...current,
468
- ...data
469
- };
583
+ const updated = mergeSessionUpdate(current, data);
470
584
  this.sessions.set(sessionKey, updated);
471
585
  }
586
+ async patchCredentials(userId, sessionId, data) {
587
+ const sessionKey = this.getSessionKey(userId, sessionId);
588
+ if (!this.sessions.has(sessionKey)) {
589
+ throw new Error(`Session ${sessionId} not found`);
590
+ }
591
+ const current = this.credentials.get(sessionKey) ?? { sessionId, userId };
592
+ this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
593
+ }
472
594
  async get(userId, sessionId) {
473
595
  const sessionKey = this.getSessionKey(userId, sessionId);
474
596
  return this.sessions.get(sessionKey) || null;
475
597
  }
598
+ async getCredentials(userId, sessionId) {
599
+ const sessionKey = this.getSessionKey(userId, sessionId);
600
+ if (!this.sessions.has(sessionKey)) return null;
601
+ return this.credentials.get(sessionKey) ?? { sessionId, userId };
602
+ }
603
+ async clearCredentials(userId, sessionId) {
604
+ await this.patchCredentials(userId, sessionId, {
605
+ clientInformation: null,
606
+ tokens: null,
607
+ codeVerifier: null,
608
+ clientId: null,
609
+ oauthState: null
610
+ });
611
+ }
476
612
  async listIds(userId) {
477
613
  const set = this.userIdSessions.get(userId);
478
614
  return set ? Array.from(set) : [];
@@ -492,6 +628,7 @@ var MemoryStorageBackend = class {
492
628
  async delete(userId, sessionId) {
493
629
  const sessionKey = this.getSessionKey(userId, sessionId);
494
630
  this.sessions.delete(sessionKey);
631
+ this.credentials.delete(sessionKey);
495
632
  const set = this.userIdSessions.get(userId);
496
633
  if (set) {
497
634
  set.delete(sessionId);
@@ -505,9 +642,22 @@ var MemoryStorageBackend = class {
505
642
  }
506
643
  async clearAll() {
507
644
  this.sessions.clear();
645
+ this.credentials.clear();
508
646
  this.userIdSessions.clear();
509
647
  }
510
648
  async cleanupExpired() {
649
+ for (const [key, session] of this.sessions.entries()) {
650
+ if (!isSessionExpired(session)) continue;
651
+ this.sessions.delete(key);
652
+ this.credentials.delete(key);
653
+ const set = this.userIdSessions.get(session.userId);
654
+ if (set) {
655
+ set.delete(session.sessionId);
656
+ if (set.size === 0) {
657
+ this.userIdSessions.delete(session.userId);
658
+ }
659
+ }
660
+ }
511
661
  }
512
662
  async disconnect() {
513
663
  }
@@ -522,6 +672,7 @@ var FileStorageBackend = class {
522
672
  constructor(options = {}) {
523
673
  __publicField(this, "filePath");
524
674
  __publicField(this, "memoryCache", null);
675
+ __publicField(this, "credentialsCache", null);
525
676
  __publicField(this, "initialized", false);
526
677
  this.filePath = options.path || "./sessions.json";
527
678
  }
@@ -536,14 +687,22 @@ var FileStorageBackend = class {
536
687
  const data = await fs2.promises.readFile(this.filePath, "utf-8");
537
688
  const json = JSON.parse(data);
538
689
  this.memoryCache = /* @__PURE__ */ new Map();
539
- if (Array.isArray(json)) {
540
- json.forEach((s) => {
541
- this.memoryCache.set(this.getSessionKey(s.userId || "unknown", s.sessionId), s);
690
+ this.credentialsCache = /* @__PURE__ */ new Map();
691
+ if (Array.isArray(json.sessions)) {
692
+ json.sessions.forEach((s) => {
693
+ const session = normalizeStoredSession(s);
694
+ this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
695
+ });
696
+ }
697
+ if (Array.isArray(json.credentials)) {
698
+ json.credentials.forEach((c) => {
699
+ this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
542
700
  });
543
701
  }
544
702
  } catch (error) {
545
703
  if (error.code === "ENOENT") {
546
704
  this.memoryCache = /* @__PURE__ */ new Map();
705
+ this.credentialsCache = /* @__PURE__ */ new Map();
547
706
  await this.flush();
548
707
  } else {
549
708
  console.error("[FileStorage] Failed to load sessions:", error);
@@ -557,9 +716,11 @@ var FileStorageBackend = class {
557
716
  if (!this.initialized) await this.init();
558
717
  }
559
718
  async flush() {
560
- if (!this.memoryCache) return;
561
- const sessions2 = Array.from(this.memoryCache.values());
562
- await fs2.promises.writeFile(this.filePath, JSON.stringify(sessions2, null, 2), "utf-8");
719
+ if (!this.memoryCache || !this.credentialsCache) return;
720
+ await fs2.promises.writeFile(this.filePath, JSON.stringify({
721
+ sessions: Array.from(this.memoryCache.values()),
722
+ credentials: Array.from(this.credentialsCache.values())
723
+ }, null, 2), "utf-8");
563
724
  }
564
725
  getSessionKey(userId, sessionId) {
565
726
  return `${userId}:${sessionId}`;
@@ -567,7 +728,7 @@ var FileStorageBackend = class {
567
728
  generateSessionId() {
568
729
  return generateSessionId();
569
730
  }
570
- async create(session, ttl) {
731
+ async create(session) {
571
732
  await this.ensureInitialized();
572
733
  const { sessionId, userId } = session;
573
734
  if (!sessionId || !userId) throw new Error("userId and sessionId required");
@@ -575,10 +736,10 @@ var FileStorageBackend = class {
575
736
  if (this.memoryCache.has(sessionKey)) {
576
737
  throw new Error(`Session ${sessionId} already exists`);
577
738
  }
578
- this.memoryCache.set(sessionKey, session);
739
+ this.memoryCache.set(sessionKey, normalizeNewSession(session));
579
740
  await this.flush();
580
741
  }
581
- async update(userId, sessionId, data, ttl) {
742
+ async update(userId, sessionId, data) {
582
743
  await this.ensureInitialized();
583
744
  if (!userId || !sessionId) throw new Error("userId and sessionId required");
584
745
  const sessionKey = this.getSessionKey(userId, sessionId);
@@ -586,18 +747,40 @@ var FileStorageBackend = class {
586
747
  if (!current) {
587
748
  throw new Error(`Session ${sessionId} not found`);
588
749
  }
589
- const updated = {
590
- ...current,
591
- ...data
592
- };
750
+ const updated = mergeSessionUpdate(current, data);
593
751
  this.memoryCache.set(sessionKey, updated);
594
752
  await this.flush();
595
753
  }
754
+ async patchCredentials(userId, sessionId, data) {
755
+ await this.ensureInitialized();
756
+ const sessionKey = this.getSessionKey(userId, sessionId);
757
+ if (!this.memoryCache.has(sessionKey)) {
758
+ throw new Error(`Session ${sessionId} not found`);
759
+ }
760
+ const current = this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
761
+ this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
762
+ await this.flush();
763
+ }
596
764
  async get(userId, sessionId) {
597
765
  await this.ensureInitialized();
598
766
  const sessionKey = this.getSessionKey(userId, sessionId);
599
767
  return this.memoryCache.get(sessionKey) || null;
600
768
  }
769
+ async getCredentials(userId, sessionId) {
770
+ await this.ensureInitialized();
771
+ const sessionKey = this.getSessionKey(userId, sessionId);
772
+ if (!this.memoryCache.has(sessionKey)) return null;
773
+ return this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
774
+ }
775
+ async clearCredentials(userId, sessionId) {
776
+ await this.patchCredentials(userId, sessionId, {
777
+ clientInformation: null,
778
+ tokens: null,
779
+ codeVerifier: null,
780
+ clientId: null,
781
+ oauthState: null
782
+ });
783
+ }
601
784
  async list(userId) {
602
785
  await this.ensureInitialized();
603
786
  return Array.from(this.memoryCache.values()).filter((s) => s.userId === userId);
@@ -609,7 +792,9 @@ var FileStorageBackend = class {
609
792
  async delete(userId, sessionId) {
610
793
  await this.ensureInitialized();
611
794
  const sessionKey = this.getSessionKey(userId, sessionId);
612
- if (this.memoryCache.delete(sessionKey)) {
795
+ const deleted = this.memoryCache.delete(sessionKey);
796
+ this.credentialsCache.delete(sessionKey);
797
+ if (deleted) {
613
798
  await this.flush();
614
799
  }
615
800
  }
@@ -620,10 +805,21 @@ var FileStorageBackend = class {
620
805
  async clearAll() {
621
806
  await this.ensureInitialized();
622
807
  this.memoryCache.clear();
808
+ this.credentialsCache.clear();
623
809
  await this.flush();
624
810
  }
625
811
  async cleanupExpired() {
626
812
  await this.ensureInitialized();
813
+ let changed = false;
814
+ for (const [key, session] of this.memoryCache.entries()) {
815
+ if (!isSessionExpired(session)) continue;
816
+ this.memoryCache.delete(key);
817
+ this.credentialsCache.delete(key);
818
+ changed = true;
819
+ }
820
+ if (changed) {
821
+ await this.flush();
822
+ }
627
823
  }
628
824
  async disconnect() {
629
825
  }
@@ -635,10 +831,12 @@ var SqliteStorage = class {
635
831
  constructor(options = {}) {
636
832
  __publicField(this, "db", null);
637
833
  __publicField(this, "table");
834
+ __publicField(this, "credentialsTable");
638
835
  __publicField(this, "initialized", false);
639
836
  __publicField(this, "dbPath");
640
837
  this.dbPath = options.path || "./sessions.db";
641
838
  this.table = options.table || "mcp_sessions";
839
+ this.credentialsTable = `${this.table}_credentials`;
642
840
  }
643
841
  async init() {
644
842
  if (this.initialized) return;
@@ -649,6 +847,7 @@ var SqliteStorage = class {
649
847
  fs2__namespace.mkdirSync(dir, { recursive: true });
650
848
  }
651
849
  this.db = new DatabaseConstructor(this.dbPath);
850
+ this.db.pragma("foreign_keys = ON");
652
851
  this.db.exec(`
653
852
  CREATE TABLE IF NOT EXISTS ${this.table} (
654
853
  sessionId TEXT PRIMARY KEY,
@@ -657,6 +856,13 @@ var SqliteStorage = class {
657
856
  expiresAt INTEGER
658
857
  );
659
858
  CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
859
+ CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
860
+ sessionId TEXT NOT NULL,
861
+ userId TEXT NOT NULL,
862
+ data TEXT NOT NULL,
863
+ PRIMARY KEY (userId, sessionId),
864
+ FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
865
+ );
660
866
  `);
661
867
  this.initialized = true;
662
868
  console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
@@ -677,18 +883,18 @@ var SqliteStorage = class {
677
883
  generateSessionId() {
678
884
  return generateSessionId();
679
885
  }
680
- async create(session, ttl) {
886
+ async create(session) {
681
887
  this.ensureInitialized();
682
888
  const { sessionId, userId } = session;
683
889
  if (!sessionId || !userId) {
684
890
  throw new Error("userId and sessionId required");
685
891
  }
686
- const expiresAt = ttl ? Date.now() + ttl * 1e3 : null;
892
+ const sessionWithLifecycle = normalizeNewSession(session);
687
893
  try {
688
894
  const stmt = this.db.prepare(
689
895
  `INSERT INTO ${this.table} (sessionId, userId, data, expiresAt) VALUES (?, ?, ?, ?)`
690
896
  );
691
- stmt.run(sessionId, userId, JSON.stringify(session), expiresAt);
897
+ stmt.run(sessionId, userId, JSON.stringify(sessionWithLifecycle), sessionWithLifecycle.expiresAt ?? null);
692
898
  } catch (error) {
693
899
  if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
694
900
  throw new Error(`Session ${sessionId} already exists`);
@@ -696,7 +902,7 @@ var SqliteStorage = class {
696
902
  throw error;
697
903
  }
698
904
  }
699
- async update(userId, sessionId, data, ttl) {
905
+ async update(userId, sessionId, data) {
700
906
  this.ensureInitialized();
701
907
  if (!sessionId || !userId) {
702
908
  throw new Error("userId and sessionId required");
@@ -705,12 +911,25 @@ var SqliteStorage = class {
705
911
  if (!currentSession) {
706
912
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
707
913
  }
708
- const updatedSession = { ...currentSession, ...data };
709
- const expiresAt = ttl ? Date.now() + ttl * 1e3 : null;
914
+ const updatedSession = mergeSessionUpdate(currentSession, data);
710
915
  const stmt = this.db.prepare(
711
916
  `UPDATE ${this.table} SET data = ?, expiresAt = ? WHERE sessionId = ? AND userId = ?`
712
917
  );
713
- stmt.run(JSON.stringify(updatedSession), expiresAt, sessionId, userId);
918
+ stmt.run(JSON.stringify(updatedSession), updatedSession.expiresAt ?? null, sessionId, userId);
919
+ }
920
+ async patchCredentials(userId, sessionId, data) {
921
+ this.ensureInitialized();
922
+ if (!await this.get(userId, sessionId)) {
923
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
924
+ }
925
+ const current = await this.getCredentials(userId, sessionId) ?? { sessionId, userId };
926
+ const credentials = { ...current, ...data, sessionId, userId };
927
+ const stmt = this.db.prepare(
928
+ `INSERT INTO ${this.credentialsTable} (sessionId, userId, data)
929
+ VALUES (?, ?, ?)
930
+ ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
931
+ );
932
+ stmt.run(sessionId, userId, JSON.stringify(credentials));
714
933
  }
715
934
  async get(userId, sessionId) {
716
935
  this.ensureInitialized();
@@ -719,7 +938,25 @@ var SqliteStorage = class {
719
938
  );
720
939
  const row = stmt.get(sessionId, userId);
721
940
  if (!row) return null;
722
- return JSON.parse(row.data);
941
+ return normalizeStoredSession(JSON.parse(row.data));
942
+ }
943
+ async getCredentials(userId, sessionId) {
944
+ this.ensureInitialized();
945
+ if (!await this.get(userId, sessionId)) return null;
946
+ const stmt = this.db.prepare(
947
+ `SELECT data FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
948
+ );
949
+ const row = stmt.get(sessionId, userId);
950
+ return row ? JSON.parse(row.data) : { sessionId, userId };
951
+ }
952
+ async clearCredentials(userId, sessionId) {
953
+ await this.patchCredentials(userId, sessionId, {
954
+ clientInformation: null,
955
+ tokens: null,
956
+ codeVerifier: null,
957
+ clientId: null,
958
+ oauthState: null
959
+ });
723
960
  }
724
961
  async list(userId) {
725
962
  this.ensureInitialized();
@@ -727,7 +964,7 @@ var SqliteStorage = class {
727
964
  `SELECT data FROM ${this.table} WHERE userId = ?`
728
965
  );
729
966
  const rows = stmt.all(userId);
730
- return rows.map((row) => JSON.parse(row.data));
967
+ return rows.map((row) => normalizeStoredSession(JSON.parse(row.data)));
731
968
  }
732
969
  async listIds(userId) {
733
970
  this.ensureInitialized();
@@ -742,6 +979,9 @@ var SqliteStorage = class {
742
979
  const stmt = this.db.prepare(
743
980
  `DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
744
981
  );
982
+ this.db.prepare(
983
+ `DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
984
+ ).run(sessionId, userId);
745
985
  stmt.run(sessionId, userId);
746
986
  }
747
987
  async listAllIds() {
@@ -752,16 +992,28 @@ var SqliteStorage = class {
752
992
  }
753
993
  async clearAll() {
754
994
  this.ensureInitialized();
995
+ this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
755
996
  const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
756
997
  stmt.run();
757
998
  }
758
999
  async cleanupExpired() {
759
1000
  this.ensureInitialized();
760
- const now = Date.now();
761
- const stmt = this.db.prepare(
762
- `DELETE FROM ${this.table} WHERE expiresAt IS NOT NULL AND expiresAt < ?`
763
- );
764
- stmt.run(now);
1001
+ const rows = this.db.prepare(`SELECT sessionId, userId, data FROM ${this.table}`).all();
1002
+ const deleteStmt = this.db.prepare(`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`);
1003
+ for (const row of rows) {
1004
+ const session = normalizeStoredSession(JSON.parse(row.data));
1005
+ if (isSessionExpired(session)) {
1006
+ deleteStmt.run(row.sessionId, row.userId);
1007
+ }
1008
+ }
1009
+ this.db.prepare(
1010
+ `DELETE FROM ${this.credentialsTable}
1011
+ WHERE NOT EXISTS (
1012
+ SELECT 1 FROM ${this.table}
1013
+ WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
1014
+ AND ${this.table}.userId = ${this.credentialsTable}.userId
1015
+ )`
1016
+ ).run();
765
1017
  }
766
1018
  async disconnect() {
767
1019
  if (this.db) {
@@ -846,19 +1098,21 @@ function decryptObject(data) {
846
1098
  var SupabaseStorageBackend = class {
847
1099
  constructor(supabase) {
848
1100
  this.supabase = supabase;
849
- __publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
850
1101
  }
851
1102
  async init() {
852
- const { error } = await this.supabase.from("mcp_sessions").select("session_id").limit(0);
853
- if (error) {
854
- if (error.code === "42P01") {
855
- throw new Error(
856
- '[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.'
857
- );
858
- }
859
- throw new Error(`[SupabaseStorage] Initialization check failed: ${error.message}`);
1103
+ await this.assertTable("mcp_sessions", "session_id");
1104
+ await this.assertTable("mcp_credentials", "session_id");
1105
+ console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
1106
+ }
1107
+ async assertTable(table, column) {
1108
+ const { error } = await this.supabase.from(table).select(column).limit(0);
1109
+ if (!error) return;
1110
+ if (error.code === "42P01") {
1111
+ throw new Error(
1112
+ `[SupabaseStorage] Table "${table}" not found in your database. Please run "npx mcp-ts supabase-init" to set up the required storage schema.`
1113
+ );
860
1114
  }
861
- console.log('[mcp-ts][Storage] Supabase: \u2713 "mcp_sessions" table verified.');
1115
+ throw new Error(`[SupabaseStorage] Initialization check failed for "${table}": ${error.message}`);
862
1116
  }
863
1117
  generateSessionId() {
864
1118
  return generateSessionId();
@@ -872,37 +1126,49 @@ var SupabaseStorageBackend = class {
872
1126
  transportType: row.transport_type,
873
1127
  callbackUrl: row.callback_url,
874
1128
  createdAt: new Date(row.created_at).getTime(),
1129
+ updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
1130
+ expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
875
1131
  userId: row.user_id,
876
1132
  headers: decryptObject(row.headers),
877
- active: row.active,
878
- clientInformation: row.client_information,
879
- tokens: decryptObject(row.tokens),
880
- codeVerifier: row.code_verifier,
881
- clientId: row.client_id
1133
+ authUrl: row.auth_url,
1134
+ status: row.status ?? "pending"
1135
+ };
1136
+ }
1137
+ mapRowToCredentials(row, userId, sessionId) {
1138
+ return {
1139
+ sessionId,
1140
+ userId,
1141
+ clientInformation: decryptObject(row?.client_information),
1142
+ tokens: decryptObject(row?.tokens),
1143
+ codeVerifier: decryptObject(row?.code_verifier),
1144
+ clientId: row?.client_id,
1145
+ oauthState: row?.oauth_state
882
1146
  };
883
1147
  }
884
- async create(session, ttl) {
1148
+ hasCredentialData(data) {
1149
+ return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
1150
+ }
1151
+ async create(session) {
885
1152
  const { sessionId, userId } = session;
886
1153
  if (!sessionId || !userId) throw new Error("userId and sessionId required");
887
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
888
- const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
1154
+ const status = session.status ?? "pending";
1155
+ const createdAt = new Date(session.createdAt || Date.now()).toISOString();
1156
+ const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
1157
+ const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
889
1158
  const { error } = await this.supabase.from("mcp_sessions").insert({
890
1159
  session_id: sessionId,
891
1160
  user_id: userId,
892
- // Maps user_id to userId to support RLS using auth.uid()
893
1161
  server_id: session.serverId,
894
1162
  server_name: session.serverName,
895
1163
  server_url: session.serverUrl,
896
1164
  transport_type: session.transportType,
897
1165
  callback_url: session.callbackUrl,
898
- created_at: new Date(session.createdAt || Date.now()).toISOString(),
1166
+ created_at: createdAt,
1167
+ updated_at: updatedAt,
899
1168
  headers: encryptObject(session.headers),
900
- active: session.active ?? false,
901
- client_information: session.clientInformation,
902
- tokens: encryptObject(session.tokens),
903
- code_verifier: session.codeVerifier,
904
- client_id: session.clientId,
905
- expires_at: expiresAt
1169
+ auth_url: session.authUrl ?? null,
1170
+ status,
1171
+ expires_at: expiresAt === null ? null : new Date(expiresAt).toISOString()
906
1172
  });
907
1173
  if (error) {
908
1174
  if (error.code === "23505") {
@@ -911,11 +1177,8 @@ var SupabaseStorageBackend = class {
911
1177
  throw new Error(`Failed to create session in Supabase: ${error.message}`);
912
1178
  }
913
1179
  }
914
- async update(userId, sessionId, data, ttl) {
915
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
916
- const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
1180
+ async update(userId, sessionId, data) {
917
1181
  const updateData = {
918
- expires_at: expiresAt,
919
1182
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
920
1183
  };
921
1184
  if ("serverId" in data) updateData.server_id = data.serverId;
@@ -923,20 +1186,50 @@ var SupabaseStorageBackend = class {
923
1186
  if ("serverUrl" in data) updateData.server_url = data.serverUrl;
924
1187
  if ("transportType" in data) updateData.transport_type = data.transportType;
925
1188
  if ("callbackUrl" in data) updateData.callback_url = data.callbackUrl;
926
- if ("active" in data) updateData.active = data.active;
1189
+ if ("status" in data) {
1190
+ const status = data.status ?? "pending";
1191
+ const expiresAt = resolveSessionExpiresAt(status);
1192
+ updateData.status = status;
1193
+ updateData.expires_at = expiresAt === null ? null : new Date(expiresAt).toISOString();
1194
+ }
927
1195
  if ("headers" in data) updateData.headers = encryptObject(data.headers);
928
- if ("clientInformation" in data) updateData.client_information = data.clientInformation;
929
- if ("tokens" in data) updateData.tokens = data.tokens === void 0 ? null : encryptObject(data.tokens);
930
- if ("codeVerifier" in data) updateData.code_verifier = data.codeVerifier;
931
- if ("clientId" in data) updateData.client_id = data.clientId;
932
- const { data: updatedRows, error } = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId).select("id");
933
- if (error) {
934
- throw new Error(`Failed to update session: ${error.message}`);
1196
+ if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
1197
+ const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
1198
+ let updatedRows = null;
1199
+ if (shouldUpdateSession) {
1200
+ const result = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId).select("id");
1201
+ if (result.error) {
1202
+ throw new Error(`Failed to update session: ${result.error.message}`);
1203
+ }
1204
+ updatedRows = result.data;
1205
+ } else {
1206
+ const result = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
1207
+ if (result.error) {
1208
+ throw new Error(`Failed to update session: ${result.error.message}`);
1209
+ }
1210
+ updatedRows = result.data;
935
1211
  }
936
1212
  if (!updatedRows || updatedRows.length === 0) {
937
1213
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
938
1214
  }
939
1215
  }
1216
+ async patchCredentials(userId, sessionId, data) {
1217
+ if (!this.hasCredentialData(data)) return;
1218
+ const row = {
1219
+ user_id: userId,
1220
+ session_id: sessionId,
1221
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1222
+ };
1223
+ if ("clientInformation" in data) row.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
1224
+ if ("tokens" in data) row.tokens = data.tokens == null ? null : encryptObject(data.tokens);
1225
+ if ("codeVerifier" in data) row.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
1226
+ if ("clientId" in data) row.client_id = data.clientId ?? null;
1227
+ if ("oauthState" in data) row.oauth_state = data.oauthState ?? null;
1228
+ const { error } = await this.supabase.from("mcp_credentials").upsert(row, { onConflict: "user_id,session_id" });
1229
+ if (error) {
1230
+ throw new Error(`Failed to update credentials: ${error.message}`);
1231
+ }
1232
+ }
940
1233
  async get(userId, sessionId) {
941
1234
  const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
942
1235
  if (error) {
@@ -946,6 +1239,21 @@ var SupabaseStorageBackend = class {
946
1239
  if (!data) return null;
947
1240
  return this.mapRowToSessionData(data);
948
1241
  }
1242
+ async getCredentials(userId, sessionId) {
1243
+ const { data, error } = await this.supabase.from("mcp_credentials").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
1244
+ if (error) {
1245
+ console.error("[SupabaseStorage] Failed to get credentials:", error);
1246
+ return null;
1247
+ }
1248
+ if (data) {
1249
+ return this.mapRowToCredentials(data, userId, sessionId);
1250
+ }
1251
+ const { data: sessionRows, error: sessionError } = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
1252
+ if (sessionError || !sessionRows || sessionRows.length === 0) {
1253
+ return null;
1254
+ }
1255
+ return { sessionId, userId };
1256
+ }
949
1257
  async list(userId) {
950
1258
  const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
951
1259
  if (error) {
@@ -954,6 +1262,12 @@ var SupabaseStorageBackend = class {
954
1262
  }
955
1263
  return data.map((row) => this.mapRowToSessionData(row));
956
1264
  }
1265
+ async clearCredentials(userId, sessionId) {
1266
+ const { error } = await this.supabase.from("mcp_credentials").delete().eq("user_id", userId).eq("session_id", sessionId);
1267
+ if (error) {
1268
+ throw new Error(`Failed to clear credentials: ${error.message}`);
1269
+ }
1270
+ }
957
1271
  async delete(userId, sessionId) {
958
1272
  const { error } = await this.supabase.from("mcp_sessions").delete().eq("user_id", userId).eq("session_id", sessionId);
959
1273
  if (error) {
@@ -977,15 +1291,24 @@ var SupabaseStorageBackend = class {
977
1291
  return data.map((row) => row.session_id);
978
1292
  }
979
1293
  async clearAll() {
1294
+ const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
1295
+ if (credentialsError) {
1296
+ console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
1297
+ }
980
1298
  const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
981
1299
  if (error) {
982
1300
  console.error("[SupabaseStorage] Failed to clear sessions:", error);
983
1301
  }
984
1302
  }
985
1303
  async cleanupExpired() {
986
- const { error } = await this.supabase.from("mcp_sessions").delete().lt("expires_at", (/* @__PURE__ */ new Date()).toISOString());
987
- if (error) {
988
- console.error("[SupabaseStorage] Failed to cleanup expired sessions:", error);
1304
+ 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());
1305
+ if (transientError) {
1306
+ console.error("[SupabaseStorage] Failed to cleanup expired inactive sessions:", transientError);
1307
+ }
1308
+ const dormantCutoff = new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString();
1309
+ const { error: dormantError } = await this.supabase.from("mcp_sessions").delete().eq("status", "active").lt("updated_at", dormantCutoff);
1310
+ if (dormantError) {
1311
+ console.error("[SupabaseStorage] Failed to cleanup dormant active sessions:", dormantError);
989
1312
  }
990
1313
  }
991
1314
  async disconnect() {
@@ -997,23 +1320,29 @@ init_cjs_shims();
997
1320
  var NeonStorageBackend = class {
998
1321
  constructor(sql, options = {}) {
999
1322
  this.sql = sql;
1000
- __publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
1001
1323
  __publicField(this, "tableName");
1324
+ __publicField(this, "credentialsTableName");
1002
1325
  const schema = options.schema || "public";
1003
1326
  const table = options.table || "mcp_sessions";
1327
+ const credentialsTable = options.credentialsTable || "mcp_credentials";
1004
1328
  this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
1329
+ this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
1005
1330
  }
1006
1331
  async init() {
1332
+ await this.assertTable(this.tableName, "mcp_sessions");
1333
+ await this.assertTable(this.credentialsTableName, "mcp_credentials");
1334
+ console.log("[mcp-ts][Storage] Neon: storage tables verified.");
1335
+ }
1336
+ async assertTable(qualifiedName, displayName) {
1007
1337
  const [{ exists } = { exists: null }] = await this.sql.query(
1008
1338
  "SELECT to_regclass($1) AS exists",
1009
- [this.tableName.replace(/"/g, "")]
1339
+ [qualifiedName.replace(/"/g, "")]
1010
1340
  );
1011
1341
  if (!exists) {
1012
1342
  throw new Error(
1013
- '[NeonStorage] Table "mcp_sessions" not found in your database. Please create it using the Neon storage guide in docs/storage-backends/neon.md.'
1343
+ `[NeonStorage] Table "${displayName}" not found in your database. Please create it using the Neon storage guide in docs/storage-backends/neon.md.`
1014
1344
  );
1015
1345
  }
1016
- console.log('[mcp-ts][Storage] Neon: "mcp_sessions" table verified.');
1017
1346
  }
1018
1347
  generateSessionId() {
1019
1348
  return generateSessionId();
@@ -1033,20 +1362,35 @@ var NeonStorageBackend = class {
1033
1362
  transportType: row.transport_type,
1034
1363
  callbackUrl: row.callback_url,
1035
1364
  createdAt: new Date(row.created_at).getTime(),
1365
+ updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
1366
+ expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
1036
1367
  userId: row.user_id,
1037
1368
  headers: decryptObject(row.headers),
1038
- active: row.active ?? false,
1039
- clientInformation: row.client_information,
1369
+ authUrl: row.auth_url ?? void 0,
1370
+ status: row.status ?? "pending"
1371
+ };
1372
+ }
1373
+ mapRowToCredentials(row, userId, sessionId) {
1374
+ return {
1375
+ sessionId,
1376
+ userId,
1377
+ clientInformation: decryptObject(row.client_information),
1040
1378
  tokens: decryptObject(row.tokens),
1041
- codeVerifier: row.code_verifier ?? void 0,
1042
- clientId: row.client_id ?? void 0
1379
+ codeVerifier: decryptObject(row.code_verifier),
1380
+ clientId: row.client_id ?? void 0,
1381
+ oauthState: row.oauth_state
1043
1382
  };
1044
1383
  }
1045
- async create(session, ttl) {
1384
+ hasCredentialData(data) {
1385
+ return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
1386
+ }
1387
+ async create(session) {
1046
1388
  const { sessionId, userId } = session;
1047
1389
  if (!sessionId || !userId) throw new Error("userId and sessionId required");
1048
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
1049
- const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
1390
+ const status = session.status ?? "pending";
1391
+ const createdAt = new Date(session.createdAt || Date.now()).toISOString();
1392
+ const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
1393
+ const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
1050
1394
  try {
1051
1395
  await this.sql.query(
1052
1396
  `INSERT INTO ${this.tableName} (
@@ -1058,16 +1402,14 @@ var NeonStorageBackend = class {
1058
1402
  transport_type,
1059
1403
  callback_url,
1060
1404
  created_at,
1405
+ updated_at,
1061
1406
  headers,
1062
- active,
1063
- client_information,
1064
- tokens,
1065
- code_verifier,
1066
- client_id,
1407
+ auth_url,
1408
+ status,
1067
1409
  expires_at
1068
1410
  ) VALUES (
1069
1411
  $1, $2, $3, $4, $5, $6, $7, $8,
1070
- $9, $10, $11, $12, $13, $14, $15
1412
+ $9, $10, $11, $12, $13
1071
1413
  )`,
1072
1414
  [
1073
1415
  sessionId,
@@ -1077,14 +1419,12 @@ var NeonStorageBackend = class {
1077
1419
  session.serverUrl,
1078
1420
  session.transportType,
1079
1421
  session.callbackUrl,
1080
- new Date(session.createdAt || Date.now()).toISOString(),
1422
+ createdAt,
1423
+ updatedAt,
1081
1424
  encryptObject(session.headers),
1082
- session.active ?? false,
1083
- session.clientInformation,
1084
- encryptObject(session.tokens),
1085
- session.codeVerifier,
1086
- session.clientId,
1087
- expiresAt
1425
+ session.authUrl ?? null,
1426
+ status,
1427
+ expiresAt === null ? null : new Date(expiresAt).toISOString()
1088
1428
  ]
1089
1429
  );
1090
1430
  } catch (error) {
@@ -1094,52 +1434,86 @@ var NeonStorageBackend = class {
1094
1434
  throw new Error(`Failed to create session in Neon: ${error.message}`);
1095
1435
  }
1096
1436
  }
1097
- async update(userId, sessionId, data, ttl) {
1437
+ async update(userId, sessionId, data) {
1098
1438
  const currentSession = await this.get(userId, sessionId);
1099
1439
  if (!currentSession) {
1100
1440
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
1101
1441
  }
1102
1442
  const updatedSession = { ...currentSession, ...data };
1103
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
1104
- const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
1105
- const updatedRows = await this.sql.query(
1106
- `UPDATE ${this.tableName}
1107
- SET
1108
- server_id = $1,
1109
- server_name = $2,
1110
- server_url = $3,
1111
- transport_type = $4,
1112
- callback_url = $5,
1113
- active = $6,
1114
- headers = $7,
1115
- client_information = $8,
1116
- tokens = $9,
1117
- code_verifier = $10,
1118
- client_id = $11,
1119
- expires_at = $12,
1120
- updated_at = now()
1121
- WHERE user_id = $13 AND session_id = $14
1122
- RETURNING id`,
1443
+ const status = updatedSession.status ?? "pending";
1444
+ const expiresAt = resolveSessionExpiresAt(status);
1445
+ 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;
1446
+ if (shouldUpdateSession) {
1447
+ const updatedRows = await this.sql.query(
1448
+ `UPDATE ${this.tableName}
1449
+ SET
1450
+ server_id = $1,
1451
+ server_name = $2,
1452
+ server_url = $3,
1453
+ transport_type = $4,
1454
+ callback_url = $5,
1455
+ status = $6,
1456
+ headers = $7,
1457
+ auth_url = $8,
1458
+ expires_at = $9,
1459
+ updated_at = now()
1460
+ WHERE user_id = $10 AND session_id = $11
1461
+ RETURNING id`,
1462
+ [
1463
+ updatedSession.serverId,
1464
+ updatedSession.serverName,
1465
+ updatedSession.serverUrl,
1466
+ updatedSession.transportType,
1467
+ updatedSession.callbackUrl,
1468
+ status,
1469
+ encryptObject(updatedSession.headers),
1470
+ updatedSession.authUrl ?? null,
1471
+ expiresAt === null ? null : new Date(expiresAt).toISOString(),
1472
+ userId,
1473
+ sessionId
1474
+ ]
1475
+ );
1476
+ if (updatedRows.length === 0) {
1477
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
1478
+ }
1479
+ }
1480
+ }
1481
+ async patchCredentials(userId, sessionId, data) {
1482
+ if (!this.hasCredentialData(data)) return;
1483
+ await this.sql.query(
1484
+ `INSERT INTO ${this.credentialsTableName} (
1485
+ user_id,
1486
+ session_id,
1487
+ client_information,
1488
+ tokens,
1489
+ code_verifier,
1490
+ client_id,
1491
+ oauth_state,
1492
+ updated_at
1493
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, now())
1494
+ ON CONFLICT (user_id, session_id)
1495
+ DO UPDATE SET
1496
+ client_information = CASE WHEN $8 THEN EXCLUDED.client_information ELSE ${this.credentialsTableName}.client_information END,
1497
+ tokens = CASE WHEN $9 THEN EXCLUDED.tokens ELSE ${this.credentialsTableName}.tokens END,
1498
+ code_verifier = CASE WHEN $10 THEN EXCLUDED.code_verifier ELSE ${this.credentialsTableName}.code_verifier END,
1499
+ client_id = CASE WHEN $11 THEN EXCLUDED.client_id ELSE ${this.credentialsTableName}.client_id END,
1500
+ oauth_state = CASE WHEN $12 THEN EXCLUDED.oauth_state ELSE ${this.credentialsTableName}.oauth_state END,
1501
+ updated_at = now()`,
1123
1502
  [
1124
- updatedSession.serverId,
1125
- updatedSession.serverName,
1126
- updatedSession.serverUrl,
1127
- updatedSession.transportType,
1128
- updatedSession.callbackUrl,
1129
- updatedSession.active ?? false,
1130
- encryptObject(updatedSession.headers),
1131
- updatedSession.clientInformation,
1132
- updatedSession.tokens === void 0 ? null : encryptObject(updatedSession.tokens),
1133
- updatedSession.codeVerifier,
1134
- updatedSession.clientId,
1135
- expiresAt,
1136
1503
  userId,
1137
- sessionId
1504
+ sessionId,
1505
+ "clientInformation" in data ? data.clientInformation == null ? null : encryptObject(data.clientInformation) : null,
1506
+ "tokens" in data ? data.tokens == null ? null : encryptObject(data.tokens) : null,
1507
+ "codeVerifier" in data ? data.codeVerifier == null ? null : encryptObject(data.codeVerifier) : null,
1508
+ "clientId" in data ? data.clientId ?? null : null,
1509
+ "oauthState" in data ? data.oauthState ?? null : null,
1510
+ "clientInformation" in data,
1511
+ "tokens" in data,
1512
+ "codeVerifier" in data,
1513
+ "clientId" in data,
1514
+ "oauthState" in data
1138
1515
  ]
1139
1516
  );
1140
- if (updatedRows.length === 0) {
1141
- throw new Error(`Session ${sessionId} not found for userId ${userId}`);
1142
- }
1143
1517
  }
1144
1518
  async get(userId, sessionId) {
1145
1519
  try {
@@ -1147,12 +1521,32 @@ var NeonStorageBackend = class {
1147
1521
  `SELECT * FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1148
1522
  [userId, sessionId]
1149
1523
  );
1150
- return rows[0] ? this.mapRowToSessionData(rows[0]) : null;
1524
+ if (!rows[0]) return null;
1525
+ return this.mapRowToSessionData(rows[0]);
1151
1526
  } catch (error) {
1152
1527
  console.error("[NeonStorage] Failed to get session:", error);
1153
1528
  return null;
1154
1529
  }
1155
1530
  }
1531
+ async getCredentials(userId, sessionId) {
1532
+ try {
1533
+ const credentialRows = await this.sql.query(
1534
+ `SELECT * FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
1535
+ [userId, sessionId]
1536
+ );
1537
+ if (credentialRows[0]) {
1538
+ return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
1539
+ }
1540
+ const sessionRows = await this.sql.query(
1541
+ `SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1542
+ [userId, sessionId]
1543
+ );
1544
+ return sessionRows[0] ? { sessionId, userId } : null;
1545
+ } catch (error) {
1546
+ console.error("[NeonStorage] Failed to get credentials:", error);
1547
+ return null;
1548
+ }
1549
+ }
1156
1550
  async list(userId) {
1157
1551
  try {
1158
1552
  const rows = await this.sql.query(
@@ -1165,6 +1559,16 @@ var NeonStorageBackend = class {
1165
1559
  return [];
1166
1560
  }
1167
1561
  }
1562
+ async clearCredentials(userId, sessionId) {
1563
+ try {
1564
+ await this.sql.query(
1565
+ `DELETE FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
1566
+ [userId, sessionId]
1567
+ );
1568
+ } catch (error) {
1569
+ console.error("[NeonStorage] Failed to clear credentials:", error);
1570
+ }
1571
+ }
1168
1572
  async delete(userId, sessionId) {
1169
1573
  try {
1170
1574
  await this.sql.query(
@@ -1200,6 +1604,7 @@ var NeonStorageBackend = class {
1200
1604
  }
1201
1605
  async clearAll() {
1202
1606
  try {
1607
+ await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
1203
1608
  await this.sql.query(`DELETE FROM ${this.tableName}`);
1204
1609
  } catch (error) {
1205
1610
  console.error("[NeonStorage] Failed to clear sessions:", error);
@@ -1208,9 +1613,17 @@ var NeonStorageBackend = class {
1208
1613
  async cleanupExpired() {
1209
1614
  try {
1210
1615
  await this.sql.query(
1211
- `DELETE FROM ${this.tableName} WHERE expires_at < $1`,
1616
+ `DELETE FROM ${this.tableName}
1617
+ WHERE expires_at IS NOT NULL
1618
+ AND expires_at < $1
1619
+ AND status <> 'active'`,
1212
1620
  [(/* @__PURE__ */ new Date()).toISOString()]
1213
1621
  );
1622
+ await this.sql.query(
1623
+ `DELETE FROM ${this.tableName}
1624
+ WHERE status = 'active' AND updated_at < $1`,
1625
+ [new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString()]
1626
+ );
1214
1627
  } catch (error) {
1215
1628
  console.error("[NeonStorage] Failed to cleanup expired sessions:", error);
1216
1629
  }
@@ -1260,26 +1673,24 @@ function emitSessionMutation(event) {
1260
1673
  function createSessionMutationEvent(prop, args) {
1261
1674
  const timestamp = Date.now();
1262
1675
  if (prop === "create") {
1263
- const [session, ttl] = args;
1676
+ const [session] = args;
1264
1677
  if (!session?.userId || !session?.sessionId) return null;
1265
1678
  return {
1266
1679
  type: "create",
1267
1680
  userId: session.userId,
1268
1681
  sessionId: session.sessionId,
1269
1682
  session,
1270
- ttl,
1271
1683
  timestamp
1272
1684
  };
1273
1685
  }
1274
1686
  if (prop === "update") {
1275
- const [userId, sessionId, patch, ttl] = args;
1687
+ const [userId, sessionId, patch] = args;
1276
1688
  if (!userId || !sessionId) return null;
1277
1689
  return {
1278
1690
  type: "update",
1279
1691
  userId,
1280
1692
  sessionId,
1281
1693
  patch,
1282
- ttl,
1283
1694
  timestamp
1284
1695
  };
1285
1696
  }
@@ -1475,7 +1886,6 @@ var StorageOAuthClientProvider = class {
1475
1886
  __publicField(this, "_authUrl");
1476
1887
  __publicField(this, "_clientId");
1477
1888
  __publicField(this, "onRedirectCallback");
1478
- __publicField(this, "tokenExpiresAt");
1479
1889
  this.userId = options.userId;
1480
1890
  this.serverId = options.serverId;
1481
1891
  this.sessionId = options.sessionId;
@@ -1509,30 +1919,30 @@ var StorageOAuthClientProvider = class {
1509
1919
  this._clientId = clientId_;
1510
1920
  }
1511
1921
  /**
1512
- * Loads OAuth data from the session store
1922
+ * Loads OAuth credentials from the session store
1513
1923
  * @private
1514
1924
  */
1515
- async getSessionData() {
1516
- const data = await sessions.get(this.userId, this.sessionId);
1925
+ async getCredentials() {
1926
+ const data = await sessions.getCredentials(this.userId, this.sessionId);
1517
1927
  if (!data) {
1518
- return {};
1928
+ return { userId: this.userId, sessionId: this.sessionId };
1519
1929
  }
1520
1930
  return data;
1521
1931
  }
1522
1932
  /**
1523
- * Saves OAuth data to the session store
1524
- * @param data - Partial OAuth data to save
1933
+ * Saves OAuth credentials to the session store
1934
+ * @param data - Partial OAuth credentials to save
1525
1935
  * @private
1526
1936
  * @throws Error if session doesn't exist (session must be created by controller layer)
1527
1937
  */
1528
- async saveSessionData(data) {
1529
- await sessions.update(this.userId, this.sessionId, data);
1938
+ async patchCredentials(data) {
1939
+ await sessions.patchCredentials(this.userId, this.sessionId, data);
1530
1940
  }
1531
1941
  /**
1532
1942
  * Retrieves stored OAuth client information
1533
1943
  */
1534
1944
  async clientInformation() {
1535
- const data = await this.getSessionData();
1945
+ const data = await this.getCredentials();
1536
1946
  if (data.clientId && !this._clientId) {
1537
1947
  this._clientId = data.clientId;
1538
1948
  }
@@ -1551,7 +1961,7 @@ var StorageOAuthClientProvider = class {
1551
1961
  * Stores OAuth client information
1552
1962
  */
1553
1963
  async saveClientInformation(clientInformation) {
1554
- await this.saveSessionData({
1964
+ await this.patchCredentials({
1555
1965
  clientInformation,
1556
1966
  clientId: clientInformation.client_id
1557
1967
  });
@@ -1561,29 +1971,58 @@ var StorageOAuthClientProvider = class {
1561
1971
  * Stores OAuth tokens
1562
1972
  */
1563
1973
  async saveTokens(tokens) {
1564
- const data = { tokens };
1565
- if (tokens.expires_in) {
1566
- this.tokenExpiresAt = Date.now() + tokens.expires_in * 1e3 - TOKEN_EXPIRY_BUFFER_MS;
1567
- }
1568
- await this.saveSessionData(data);
1974
+ await this.patchCredentials({ tokens });
1569
1975
  }
1570
1976
  get authUrl() {
1571
1977
  return this._authUrl;
1572
1978
  }
1573
1979
  async state() {
1574
- return this.sessionId;
1980
+ const nonce = nanoid.nanoid(32);
1981
+ await this.patchCredentials({
1982
+ oauthState: {
1983
+ nonce,
1984
+ sessionId: this.sessionId,
1985
+ serverId: this.serverId,
1986
+ createdAt: Date.now()
1987
+ },
1988
+ codeVerifier: null
1989
+ });
1990
+ return formatOAuthState(nonce, this.sessionId);
1575
1991
  }
1576
- async checkState(_state) {
1577
- const data = await sessions.get(this.userId, this.sessionId);
1992
+ async checkState(state) {
1993
+ const parsed = parseOAuthState(state);
1994
+ if (!parsed) {
1995
+ return { valid: false, error: "Invalid OAuth state" };
1996
+ }
1997
+ if (parsed.sessionId !== this.sessionId) {
1998
+ return { valid: false, error: "OAuth state mismatch" };
1999
+ }
2000
+ const data = await sessions.getCredentials(this.userId, parsed.sessionId);
1578
2001
  if (!data) {
1579
2002
  return { valid: false, error: "Session not found" };
1580
2003
  }
1581
- return { valid: true, serverId: this.serverId };
2004
+ const oauthState = data.oauthState;
2005
+ if (!oauthState) {
2006
+ return { valid: false, error: "OAuth state not found" };
2007
+ }
2008
+ if (oauthState.nonce !== parsed.nonce || oauthState.sessionId !== parsed.sessionId || oauthState.serverId !== this.serverId) {
2009
+ return { valid: false, error: "OAuth state mismatch" };
2010
+ }
2011
+ if (Date.now() - oauthState.createdAt > STATE_EXPIRATION_MS) {
2012
+ return { valid: false, error: "OAuth state expired" };
2013
+ }
2014
+ return { valid: true, serverId: oauthState.serverId };
1582
2015
  }
1583
- async consumeState(_state) {
2016
+ async consumeState(state) {
2017
+ const result = await this.checkState(state);
2018
+ if (!result.valid) {
2019
+ throw new Error(result.error || "Invalid OAuth state");
2020
+ }
2021
+ await this.patchCredentials({ oauthState: null });
1584
2022
  }
1585
2023
  async redirectToAuthorization(authUrl) {
1586
2024
  this._authUrl = authUrl.toString();
2025
+ await sessions.update(this.userId, this.sessionId, { authUrl: this._authUrl });
1587
2026
  if (this.onRedirectCallback) {
1588
2027
  this.onRedirectCallback(authUrl.toString());
1589
2028
  }
@@ -1594,21 +2033,25 @@ var StorageOAuthClientProvider = class {
1594
2033
  } else {
1595
2034
  const updates = {};
1596
2035
  if (scope === "client") {
1597
- updates.clientInformation = void 0;
1598
- updates.clientId = void 0;
2036
+ updates.clientInformation = null;
2037
+ updates.clientId = null;
1599
2038
  } else if (scope === "tokens") {
1600
- updates.tokens = void 0;
2039
+ updates.tokens = null;
1601
2040
  } else if (scope === "verifier") {
1602
- updates.codeVerifier = void 0;
2041
+ updates.codeVerifier = null;
1603
2042
  }
1604
- await this.saveSessionData(updates);
2043
+ await this.patchCredentials(updates);
1605
2044
  }
1606
2045
  }
1607
2046
  async saveCodeVerifier(verifier) {
1608
- await this.saveSessionData({ codeVerifier: verifier });
2047
+ const data = await this.getCredentials();
2048
+ if (data.codeVerifier) {
2049
+ return;
2050
+ }
2051
+ await this.patchCredentials({ codeVerifier: verifier });
1609
2052
  }
1610
2053
  async codeVerifier() {
1611
- const data = await this.getSessionData();
2054
+ const data = await this.getCredentials();
1612
2055
  if (data.clientId && !this._clientId) {
1613
2056
  this._clientId = data.clientId;
1614
2057
  }
@@ -1618,23 +2061,14 @@ var StorageOAuthClientProvider = class {
1618
2061
  return data.codeVerifier;
1619
2062
  }
1620
2063
  async deleteCodeVerifier() {
1621
- await this.saveSessionData({ codeVerifier: void 0 });
2064
+ await this.patchCredentials({ codeVerifier: null });
1622
2065
  }
1623
2066
  async tokens() {
1624
- const data = await this.getSessionData();
2067
+ const data = await this.getCredentials();
1625
2068
  if (data.clientId && !this._clientId) {
1626
2069
  this._clientId = data.clientId;
1627
2070
  }
1628
- return data.tokens;
1629
- }
1630
- isTokenExpired() {
1631
- if (!this.tokenExpiresAt) {
1632
- return false;
1633
- }
1634
- return Date.now() >= this.tokenExpiresAt;
1635
- }
1636
- setTokenExpiresAt(expiresAt) {
1637
- this.tokenExpiresAt = expiresAt;
2071
+ return data.tokens ?? void 0;
1638
2072
  }
1639
2073
  };
1640
2074
 
@@ -1958,17 +2392,18 @@ var MCPClient = class {
1958
2392
  }
1959
2393
  this.emitStateChange("INITIALIZING");
1960
2394
  this.emitProgress("Loading session configuration...");
2395
+ let existingSession = null;
1961
2396
  if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
1962
- const sessionData = await sessions.get(this.userId, this.sessionId);
1963
- if (!sessionData) {
2397
+ existingSession = await sessions.get(this.userId, this.sessionId);
2398
+ if (!existingSession) {
1964
2399
  throw new Error(`Session not found: ${this.sessionId}`);
1965
2400
  }
1966
- this.serverUrl = this.serverUrl || sessionData.serverUrl;
1967
- this.callbackUrl = this.callbackUrl || sessionData.callbackUrl;
1968
- this.serverName = this.serverName || sessionData.serverName;
1969
- this.serverId = this.serverId || sessionData.serverId || "unknown";
1970
- this.headers = this.headers || sessionData.headers;
1971
- this.createdAt = sessionData.createdAt;
2401
+ this.serverUrl = this.serverUrl || existingSession.serverUrl;
2402
+ this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
2403
+ this.serverName = this.serverName || existingSession.serverName;
2404
+ this.serverId = this.serverId || existingSession.serverId || "unknown";
2405
+ this.headers = this.headers || existingSession.headers;
2406
+ this.createdAt = existingSession.createdAt;
1972
2407
  }
1973
2408
  if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
1974
2409
  throw new Error("Missing required connection metadata");
@@ -2012,10 +2447,13 @@ var MCPClient = class {
2012
2447
  }
2013
2448
  );
2014
2449
  }
2015
- const existingSession = await sessions.get(this.userId, this.sessionId);
2450
+ if (existingSession === null) {
2451
+ existingSession = await sessions.get(this.userId, this.sessionId);
2452
+ }
2016
2453
  if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
2017
2454
  this.createdAt = Date.now();
2018
- console.log(`[MCPClient] Creating initial session ${this.sessionId} for OAuth flow`);
2455
+ const updatedAt = this.createdAt;
2456
+ console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
2019
2457
  await sessions.create({
2020
2458
  sessionId: this.sessionId,
2021
2459
  userId: this.userId,
@@ -2026,18 +2464,18 @@ var MCPClient = class {
2026
2464
  transportType: this.transportType || "streamable-http",
2027
2465
  headers: this.headers,
2028
2466
  createdAt: this.createdAt,
2029
- active: false
2030
- }, Math.floor(STATE_EXPIRATION_MS / 1e3));
2467
+ updatedAt,
2468
+ status: "pending"
2469
+ });
2031
2470
  }
2032
2471
  }
2033
2472
  /**
2034
2473
  * Saves current session state to the session store
2035
2474
  * Creates new session if it doesn't exist, updates if it does
2036
- * @param ttl - Time-to-live in seconds (defaults to 12hr for connected sessions)
2037
- * @param active - Session status marker used to avoid unnecessary TTL rewrites
2475
+ * @param status - Session lifecycle status used by storage cleanup
2038
2476
  * @private
2039
2477
  */
2040
- async saveSession(ttl = SESSION_TTL_SECONDS, active = true) {
2478
+ async saveSession(status = "active", existingSession) {
2041
2479
  if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
2042
2480
  return;
2043
2481
  }
@@ -2051,13 +2489,29 @@ var MCPClient = class {
2051
2489
  transportType: this.transportType || "streamable-http",
2052
2490
  headers: this.headers,
2053
2491
  createdAt: this.createdAt || Date.now(),
2054
- active
2492
+ updatedAt: Date.now(),
2493
+ status
2055
2494
  };
2056
- const existingSession = await sessions.get(this.userId, this.sessionId);
2495
+ if (status === "active") {
2496
+ sessionData.authUrl = null;
2497
+ }
2498
+ if (existingSession === void 0) {
2499
+ existingSession = await sessions.get(this.userId, this.sessionId);
2500
+ }
2057
2501
  if (existingSession) {
2058
- await sessions.update(this.userId, this.sessionId, sessionData, ttl);
2502
+ await sessions.update(this.userId, this.sessionId, sessionData);
2059
2503
  } else {
2060
- await sessions.create(sessionData, ttl);
2504
+ await sessions.create(sessionData);
2505
+ }
2506
+ }
2507
+ /**
2508
+ * Removes transient setup/auth sessions without masking the original error.
2509
+ * @private
2510
+ */
2511
+ async deleteTransientSession() {
2512
+ try {
2513
+ await sessions.delete(this.userId, this.sessionId);
2514
+ } catch {
2061
2515
  }
2062
2516
  }
2063
2517
  /**
@@ -2122,8 +2576,8 @@ var MCPClient = class {
2122
2576
  this.transportType = transportType;
2123
2577
  this.emitStateChange("CONNECTED");
2124
2578
  this.emitProgress("Connected successfully");
2125
- console.log(`[MCPClient] Saving session ${this.sessionId} with 12hr TTL (connect success)`);
2126
- await this.saveSession(SESSION_TTL_SECONDS, true);
2579
+ console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
2580
+ await this.saveSession("active");
2127
2581
  } catch (error) {
2128
2582
  if (error instanceof auth_js.UnauthorizedError || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
2129
2583
  let authUrl = "";
@@ -2135,15 +2589,12 @@ var MCPClient = class {
2135
2589
  const message = detail.toLowerCase() === "unauthorized" ? "OAuth authorization URL not available" : `OAuth authorization URL not available: ${detail}`;
2136
2590
  this.emitError(message, "auth");
2137
2591
  this.emitStateChange("FAILED");
2138
- try {
2139
- await sessions.delete(this.userId, this.sessionId);
2140
- } catch {
2141
- }
2592
+ await this.deleteTransientSession();
2142
2593
  throw new Error(message);
2143
2594
  }
2144
2595
  this.emitStateChange("AUTHENTICATING");
2145
- console.log(`[MCPClient] Saving session ${this.sessionId} with 10min TTL (OAuth pending)`);
2146
- await this.saveSession(Math.floor(STATE_EXPIRATION_MS / 1e3), false);
2596
+ console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
2597
+ await this.saveSession("pending");
2147
2598
  if (this.serverId) {
2148
2599
  this._onConnectionEvent.fire({
2149
2600
  type: "auth_required",
@@ -2163,7 +2614,7 @@ var MCPClient = class {
2163
2614
  this.emitStateChange("FAILED");
2164
2615
  try {
2165
2616
  const existingSession = await sessions.get(this.userId, this.sessionId);
2166
- if (!existingSession || existingSession.active !== true) {
2617
+ if (!existingSession || existingSession.status !== "active") {
2167
2618
  await sessions.delete(this.userId, this.sessionId);
2168
2619
  }
2169
2620
  } catch {
@@ -2178,7 +2629,7 @@ var MCPClient = class {
2178
2629
  * @param authCode - Authorization code received from OAuth callback
2179
2630
  */
2180
2631
  // TODO: needs to be optimized
2181
- async finishAuth(authCode) {
2632
+ async finishAuth(authCode, state) {
2182
2633
  this.emitStateChange("AUTHENTICATING");
2183
2634
  this.emitProgress("Exchanging authorization code for tokens...");
2184
2635
  await this.initialize();
@@ -2188,6 +2639,16 @@ var MCPClient = class {
2188
2639
  this.emitStateChange("FAILED");
2189
2640
  throw new Error(error);
2190
2641
  }
2642
+ if (state) {
2643
+ const stateCheck = await this.oauthProvider.checkState(state);
2644
+ if (!stateCheck.valid) {
2645
+ const error = stateCheck.error || "Invalid OAuth state";
2646
+ this.emitError(error, "auth");
2647
+ this.emitStateChange("FAILED");
2648
+ throw new Error(error);
2649
+ }
2650
+ await this.oauthProvider.consumeState(state);
2651
+ }
2191
2652
  const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
2192
2653
  let lastError;
2193
2654
  let tokensExchanged = false;
@@ -2227,8 +2688,8 @@ var MCPClient = class {
2227
2688
  await this.client.connect(this.transport);
2228
2689
  this.transportType = currentType;
2229
2690
  this.emitStateChange("CONNECTED");
2230
- console.log(`[MCPClient] Updating session ${this.sessionId} to 12hr TTL (OAuth complete)`);
2231
- await this.saveSession(SESSION_TTL_SECONDS, true);
2691
+ console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
2692
+ await this.saveSession("active");
2232
2693
  return;
2233
2694
  } catch (error) {
2234
2695
  lastError = error;
@@ -2241,12 +2702,14 @@ var MCPClient = class {
2241
2702
  const msg = error instanceof Error ? error.message : "Authentication failed";
2242
2703
  this.emitError(msg, "auth");
2243
2704
  this.emitStateChange("FAILED");
2705
+ await this.deleteTransientSession();
2244
2706
  throw error;
2245
2707
  }
2246
2708
  if (isLastAttempt) {
2247
2709
  const msg = error instanceof Error ? error.message : "Authentication failed";
2248
2710
  this.emitError(msg, "auth");
2249
2711
  this.emitStateChange("FAILED");
2712
+ await this.deleteTransientSession();
2250
2713
  throw error;
2251
2714
  }
2252
2715
  this.emitProgress(`Auth attempt with ${currentType} failed: ${errorMessage}. Retrying...`);
@@ -2256,6 +2719,7 @@ var MCPClient = class {
2256
2719
  const errorMessage = lastError instanceof Error ? lastError.message : "Authentication failed";
2257
2720
  this.emitError(errorMessage, "auth");
2258
2721
  this.emitStateChange("FAILED");
2722
+ await this.deleteTransientSession();
2259
2723
  throw lastError;
2260
2724
  }
2261
2725
  }
@@ -2570,48 +3034,6 @@ var MCPClient = class {
2570
3034
  getSessionId() {
2571
3035
  return this.sessionId;
2572
3036
  }
2573
- /**
2574
- * Gets MCP server configuration for all active user sessions
2575
- * Loads sessions from storage and returns server connection metadata.
2576
- * OAuth refresh is handled by SDK transports through their authProvider.
2577
- * @deprecated This returns legacy connection metadata only and does not
2578
- * include OAuth tokens or generated Authorization headers. Prefer
2579
- * MultiSessionClient or explicit MCPClient instances so SDK transports can
2580
- * own OAuth refresh and reauthorization.
2581
- * @param userId - User ID to fetch sessions for
2582
- * @returns Object keyed by sanitized server labels containing transport and url.
2583
- * @static
2584
- */
2585
- static async getMcpServerConfig(userId) {
2586
- const mcpConfig = {};
2587
- const sessionList = await sessions.list(userId);
2588
- await Promise.all(
2589
- sessionList.map(async (sessionData) => {
2590
- const { sessionId } = sessionData;
2591
- try {
2592
- if (!sessionData.serverId || !sessionData.transportType || !sessionData.serverUrl || !sessionData.callbackUrl) {
2593
- await sessions.delete(userId, sessionId);
2594
- return;
2595
- }
2596
- const label = sanitizeServerLabel(
2597
- sessionData.serverName || sessionData.serverId || "server"
2598
- );
2599
- mcpConfig[label] = {
2600
- transport: sessionData.transportType,
2601
- url: sessionData.serverUrl,
2602
- ...sessionData.serverName && {
2603
- serverName: sessionData.serverName,
2604
- serverLabel: label
2605
- }
2606
- };
2607
- } catch (error) {
2608
- await sessions.delete(userId, sessionId);
2609
- console.warn(`[MCP] Failed to process session ${sessionId}:`, error);
2610
- }
2611
- })
2612
- );
2613
- return mcpConfig;
2614
- }
2615
3037
  };
2616
3038
 
2617
3039
  // src/server/mcp/multi-session-client.ts
@@ -2647,18 +3069,13 @@ var MultiSessionClient = class {
2647
3069
  *
2648
3070
  * A session is considered connectable when:
2649
3071
  * - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
2650
- * - Its `active` flag is not explicitly `false` sessions with `active: false` are
2651
- * either mid-OAuth flow, auth-pending, or previously failed. We skip those here
3072
+ * - Its status is `active`. Pending sessions are skipped here
2652
3073
  * and let the OAuth flow complete separately before we try to reconnect them.
2653
- *
2654
- * Note: Sessions where `active` is `undefined` (legacy records) are included
2655
- * for backwards compatibility.
2656
3074
  */
2657
3075
  async getActiveSessions() {
2658
3076
  const sessionList = await sessions.list(this.userId);
2659
3077
  const valid = sessionList.filter(
2660
- (s) => s.serverId && s.serverUrl && s.callbackUrl && s.active !== false
2661
- // exclude OAuth-pending / failed sessions
3078
+ (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
2662
3079
  );
2663
3080
  return valid;
2664
3081
  }
@@ -2933,7 +3350,8 @@ var SSEConnectionManager = class {
2933
3350
  serverUrl: s.serverUrl,
2934
3351
  transport: s.transportType,
2935
3352
  createdAt: s.createdAt,
2936
- active: s.active !== false
3353
+ updatedAt: s.updatedAt ?? s.createdAt,
3354
+ status: s.status ?? "pending"
2937
3355
  }))
2938
3356
  };
2939
3357
  }
@@ -2949,7 +3367,7 @@ var SSEConnectionManager = class {
2949
3367
  (s) => s.serverId === serverId || s.serverUrl === serverUrl
2950
3368
  );
2951
3369
  if (duplicate) {
2952
- if (duplicate.active === false) {
3370
+ if (duplicate.status === "pending") {
2953
3371
  await this.getSession({ sessionId: duplicate.sessionId });
2954
3372
  return {
2955
3373
  sessionId: duplicate.sessionId,
@@ -3136,7 +3554,10 @@ var SSEConnectionManager = class {
3136
3554
  * Complete OAuth authorization flow
3137
3555
  */
3138
3556
  async finishAuth(params) {
3139
- const { sessionId, code } = params;
3557
+ const { code } = params;
3558
+ const oauthState = params.state;
3559
+ const parsedState = parseOAuthState(oauthState);
3560
+ const sessionId = parsedState?.sessionId || oauthState;
3140
3561
  const session = await sessions.get(this.userId, sessionId);
3141
3562
  if (!session) {
3142
3563
  throw new Error("Session not found");
@@ -3161,7 +3582,7 @@ var SSEConnectionManager = class {
3161
3582
  headers: session.headers
3162
3583
  });
3163
3584
  client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3164
- await client.finishAuth(code);
3585
+ await client.finishAuth(code, oauthState);
3165
3586
  this.clients.set(sessionId, client);
3166
3587
  const tools = await client.listTools();
3167
3588
  return { success: true, toolCount: tools.tools.length };
@@ -3473,8 +3894,8 @@ var SSEClient = class {
3473
3894
  async getSession(sessionId) {
3474
3895
  return this.sendRequest("getSession", { sessionId });
3475
3896
  }
3476
- async finishAuth(sessionId, code) {
3477
- return this.sendRequest("finishAuth", { sessionId, code });
3897
+ async finishAuth(state, code) {
3898
+ return this.sendRequest("finishAuth", { state, code });
3478
3899
  }
3479
3900
  async listPrompts(sessionId) {
3480
3901
  return this.sendRequest("listPrompts", { sessionId });
@@ -5357,6 +5778,8 @@ exports.DEFAULT_HEARTBEAT_INTERVAL_MS = DEFAULT_HEARTBEAT_INTERVAL_MS;
5357
5778
  exports.DEFAULT_LOGO_URI = DEFAULT_LOGO_URI;
5358
5779
  exports.DEFAULT_MCP_APP_CSP = DEFAULT_MCP_APP_CSP;
5359
5780
  exports.DEFAULT_POLICY_URI = DEFAULT_POLICY_URI;
5781
+ exports.DORMANT_SESSION_EXPIRATION_MS = DORMANT_SESSION_EXPIRATION_MS;
5782
+ exports.DORMANT_SESSION_EXPIRATION_SECONDS = DORMANT_SESSION_EXPIRATION_SECONDS;
5360
5783
  exports.DisposableStore = DisposableStore;
5361
5784
  exports.Emitter = Emitter;
5362
5785
  exports.InvalidStateError = InvalidStateError;
@@ -5366,11 +5789,11 @@ exports.MCP_CLIENT_VERSION = MCP_CLIENT_VERSION;
5366
5789
  exports.McpError = McpError;
5367
5790
  exports.MultiSessionClient = MultiSessionClient;
5368
5791
  exports.NotConnectedError = NotConnectedError;
5792
+ exports.PENDING_SESSION_EXPIRATION_SECONDS = PENDING_SESSION_EXPIRATION_SECONDS;
5369
5793
  exports.REDIS_KEY_PREFIX = REDIS_KEY_PREFIX;
5370
5794
  exports.RpcErrorCodes = RpcErrorCodes;
5371
5795
  exports.SANDBOX_PROXY_READY_METHOD = SANDBOX_PROXY_READY_METHOD;
5372
5796
  exports.SANDBOX_RESOURCE_READY_METHOD = SANDBOX_RESOURCE_READY_METHOD;
5373
- exports.SESSION_TTL_SECONDS = SESSION_TTL_SECONDS;
5374
5797
  exports.SOFTWARE_ID = SOFTWARE_ID;
5375
5798
  exports.SOFTWARE_VERSION = SOFTWARE_VERSION;
5376
5799
  exports.SSEClient = SSEClient;
@@ -5380,7 +5803,6 @@ exports.SchemaCompressor = SchemaCompressor;
5380
5803
  exports.SessionNotFoundError = SessionNotFoundError;
5381
5804
  exports.SessionValidationError = SessionValidationError;
5382
5805
  exports.StorageOAuthClientProvider = StorageOAuthClientProvider;
5383
- exports.TOKEN_EXPIRY_BUFFER_MS = TOKEN_EXPIRY_BUFFER_MS;
5384
5806
  exports.ToolExecutionError = ToolExecutionError;
5385
5807
  exports.ToolIndex = ToolIndex;
5386
5808
  exports.ToolRouter = ToolRouter;