@mcp-ts/sdk 2.3.3 → 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 +729 -397
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +724 -396
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{multi-session-client-DYNe6az3.d.mts → multi-session-client-CIMUGF8S.d.mts} +15 -41
  39. package/dist/{multi-session-client-BYtguGJm.d.ts → multi-session-client-CnvZEGPY.d.ts} +15 -41
  40. package/dist/server/index.d.mts +45 -17
  41. package/dist/server/index.d.ts +45 -17
  42. package/dist/server/index.js +721 -391
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +718 -391
  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 -220
  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
@@ -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"
1128
+ };
1129
+ }
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
876
1139
  };
877
1140
  }
878
- async create(session, ttl) {
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 = 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
- 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
 
@@ -1709,14 +2142,7 @@ var RpcErrorCodes = {
1709
2142
  EXECUTION_ERROR: "EXECUTION_ERROR"};
1710
2143
 
1711
2144
  // src/server/mcp/oauth-client.ts
1712
- function isInvalidRefreshTokenError(error) {
1713
- if (!(error instanceof Error)) {
1714
- return false;
1715
- }
1716
- const text = `${error.name} ${error.message}`.toLowerCase();
1717
- return text.includes("invalidgrant") || text.includes("invalid_grant") || text.includes("invalid refresh token") || /refresh\s+token\s+(?:is\s+)?(?:invalid|expired|revoked)/i.test(text);
1718
- }
1719
- var MCPClient = class _MCPClient {
2145
+ var MCPClient = class {
1720
2146
  /**
1721
2147
  * Creates a new MCP client instance
1722
2148
  * Can be initialized with minimal options (userId + sessionId) for session restoration
@@ -1891,17 +2317,18 @@ var MCPClient = class _MCPClient {
1891
2317
  }
1892
2318
  this.emitStateChange("INITIALIZING");
1893
2319
  this.emitProgress("Loading session configuration...");
2320
+ let existingSession = null;
1894
2321
  if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
1895
- const sessionData = await sessions.get(this.userId, this.sessionId);
1896
- if (!sessionData) {
2322
+ existingSession = await sessions.get(this.userId, this.sessionId);
2323
+ if (!existingSession) {
1897
2324
  throw new Error(`Session not found: ${this.sessionId}`);
1898
2325
  }
1899
- this.serverUrl = this.serverUrl || sessionData.serverUrl;
1900
- this.callbackUrl = this.callbackUrl || sessionData.callbackUrl;
1901
- this.serverName = this.serverName || sessionData.serverName;
1902
- this.serverId = this.serverId || sessionData.serverId || "unknown";
1903
- this.headers = this.headers || sessionData.headers;
1904
- this.createdAt = sessionData.createdAt;
2326
+ this.serverUrl = this.serverUrl || existingSession.serverUrl;
2327
+ this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
2328
+ this.serverName = this.serverName || existingSession.serverName;
2329
+ this.serverId = this.serverId || existingSession.serverId || "unknown";
2330
+ this.headers = this.headers || existingSession.headers;
2331
+ this.createdAt = existingSession.createdAt;
1905
2332
  }
1906
2333
  if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
1907
2334
  throw new Error("Missing required connection metadata");
@@ -1945,10 +2372,13 @@ var MCPClient = class _MCPClient {
1945
2372
  }
1946
2373
  );
1947
2374
  }
1948
- const existingSession = await sessions.get(this.userId, this.sessionId);
2375
+ if (existingSession === null) {
2376
+ existingSession = await sessions.get(this.userId, this.sessionId);
2377
+ }
1949
2378
  if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
1950
2379
  this.createdAt = Date.now();
1951
- console.log(`[MCPClient] Creating initial session ${this.sessionId} for OAuth flow`);
2380
+ const updatedAt = this.createdAt;
2381
+ console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
1952
2382
  await sessions.create({
1953
2383
  sessionId: this.sessionId,
1954
2384
  userId: this.userId,
@@ -1959,18 +2389,18 @@ var MCPClient = class _MCPClient {
1959
2389
  transportType: this.transportType || "streamable-http",
1960
2390
  headers: this.headers,
1961
2391
  createdAt: this.createdAt,
1962
- active: false
1963
- }, Math.floor(STATE_EXPIRATION_MS / 1e3));
2392
+ updatedAt,
2393
+ status: "pending"
2394
+ });
1964
2395
  }
1965
2396
  }
1966
2397
  /**
1967
2398
  * Saves current session state to the session store
1968
2399
  * Creates new session if it doesn't exist, updates if it does
1969
- * @param ttl - Time-to-live in seconds (defaults to 12hr for connected sessions)
1970
- * @param active - Session status marker used to avoid unnecessary TTL rewrites
2400
+ * @param status - Session lifecycle status used by storage cleanup
1971
2401
  * @private
1972
2402
  */
1973
- async saveSession(ttl = SESSION_TTL_SECONDS, active = true) {
2403
+ async saveSession(status = "active", existingSession) {
1974
2404
  if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
1975
2405
  return;
1976
2406
  }
@@ -1984,13 +2414,29 @@ var MCPClient = class _MCPClient {
1984
2414
  transportType: this.transportType || "streamable-http",
1985
2415
  headers: this.headers,
1986
2416
  createdAt: this.createdAt || Date.now(),
1987
- active
2417
+ updatedAt: Date.now(),
2418
+ status
1988
2419
  };
1989
- const existingSession = await sessions.get(this.userId, this.sessionId);
2420
+ if (status === "active") {
2421
+ sessionData.authUrl = null;
2422
+ }
2423
+ if (existingSession === void 0) {
2424
+ existingSession = await sessions.get(this.userId, this.sessionId);
2425
+ }
1990
2426
  if (existingSession) {
1991
- await sessions.update(this.userId, this.sessionId, sessionData, ttl);
2427
+ await sessions.update(this.userId, this.sessionId, sessionData);
1992
2428
  } else {
1993
- await sessions.create(sessionData, ttl);
2429
+ await sessions.create(sessionData);
2430
+ }
2431
+ }
2432
+ /**
2433
+ * Removes transient setup/auth sessions without masking the original error.
2434
+ * @private
2435
+ */
2436
+ async deleteTransientSession() {
2437
+ try {
2438
+ await sessions.delete(this.userId, this.sessionId);
2439
+ } catch {
1994
2440
  }
1995
2441
  }
1996
2442
  /**
@@ -2050,15 +2496,13 @@ var MCPClient = class _MCPClient {
2050
2496
  throw new Error(error);
2051
2497
  }
2052
2498
  try {
2053
- this.emitProgress("Validating OAuth tokens...");
2054
- await this.getValidTokens();
2055
2499
  this.emitStateChange("CONNECTING");
2056
2500
  const { transportType } = await this.tryConnect();
2057
2501
  this.transportType = transportType;
2058
2502
  this.emitStateChange("CONNECTED");
2059
2503
  this.emitProgress("Connected successfully");
2060
- console.log(`[MCPClient] Saving session ${this.sessionId} with 12hr TTL (connect success)`);
2061
- await this.saveSession(SESSION_TTL_SECONDS, true);
2504
+ console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
2505
+ await this.saveSession("active");
2062
2506
  } catch (error) {
2063
2507
  if (error instanceof auth_js.UnauthorizedError || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
2064
2508
  let authUrl = "";
@@ -2070,15 +2514,12 @@ var MCPClient = class _MCPClient {
2070
2514
  const message = detail.toLowerCase() === "unauthorized" ? "OAuth authorization URL not available" : `OAuth authorization URL not available: ${detail}`;
2071
2515
  this.emitError(message, "auth");
2072
2516
  this.emitStateChange("FAILED");
2073
- try {
2074
- await sessions.delete(this.userId, this.sessionId);
2075
- } catch {
2076
- }
2517
+ await this.deleteTransientSession();
2077
2518
  throw new Error(message);
2078
2519
  }
2079
2520
  this.emitStateChange("AUTHENTICATING");
2080
- console.log(`[MCPClient] Saving session ${this.sessionId} with 10min TTL (OAuth pending)`);
2081
- await this.saveSession(Math.floor(STATE_EXPIRATION_MS / 1e3), false);
2521
+ console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
2522
+ await this.saveSession("pending");
2082
2523
  if (this.serverId) {
2083
2524
  this._onConnectionEvent.fire({
2084
2525
  type: "auth_required",
@@ -2098,7 +2539,7 @@ var MCPClient = class _MCPClient {
2098
2539
  this.emitStateChange("FAILED");
2099
2540
  try {
2100
2541
  const existingSession = await sessions.get(this.userId, this.sessionId);
2101
- if (!existingSession || existingSession.active !== true) {
2542
+ if (!existingSession || existingSession.status !== "active") {
2102
2543
  await sessions.delete(this.userId, this.sessionId);
2103
2544
  }
2104
2545
  } catch {
@@ -2113,7 +2554,7 @@ var MCPClient = class _MCPClient {
2113
2554
  * @param authCode - Authorization code received from OAuth callback
2114
2555
  */
2115
2556
  // TODO: needs to be optimized
2116
- async finishAuth(authCode) {
2557
+ async finishAuth(authCode, state) {
2117
2558
  this.emitStateChange("AUTHENTICATING");
2118
2559
  this.emitProgress("Exchanging authorization code for tokens...");
2119
2560
  await this.initialize();
@@ -2123,6 +2564,16 @@ var MCPClient = class _MCPClient {
2123
2564
  this.emitStateChange("FAILED");
2124
2565
  throw new Error(error);
2125
2566
  }
2567
+ if (state) {
2568
+ const stateCheck = await this.oauthProvider.checkState(state);
2569
+ if (!stateCheck.valid) {
2570
+ const error = stateCheck.error || "Invalid OAuth state";
2571
+ this.emitError(error, "auth");
2572
+ this.emitStateChange("FAILED");
2573
+ throw new Error(error);
2574
+ }
2575
+ await this.oauthProvider.consumeState(state);
2576
+ }
2126
2577
  const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
2127
2578
  let lastError;
2128
2579
  let tokensExchanged = false;
@@ -2162,8 +2613,8 @@ var MCPClient = class _MCPClient {
2162
2613
  await this.client.connect(this.transport);
2163
2614
  this.transportType = currentType;
2164
2615
  this.emitStateChange("CONNECTED");
2165
- console.log(`[MCPClient] Updating session ${this.sessionId} to 12hr TTL (OAuth complete)`);
2166
- await this.saveSession(SESSION_TTL_SECONDS, true);
2616
+ console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
2617
+ await this.saveSession("active");
2167
2618
  return;
2168
2619
  } catch (error) {
2169
2620
  lastError = error;
@@ -2176,12 +2627,14 @@ var MCPClient = class _MCPClient {
2176
2627
  const msg = error instanceof Error ? error.message : "Authentication failed";
2177
2628
  this.emitError(msg, "auth");
2178
2629
  this.emitStateChange("FAILED");
2630
+ await this.deleteTransientSession();
2179
2631
  throw error;
2180
2632
  }
2181
2633
  if (isLastAttempt) {
2182
2634
  const msg = error instanceof Error ? error.message : "Authentication failed";
2183
2635
  this.emitError(msg, "auth");
2184
2636
  this.emitStateChange("FAILED");
2637
+ await this.deleteTransientSession();
2185
2638
  throw error;
2186
2639
  }
2187
2640
  this.emitProgress(`Auth attempt with ${currentType} failed: ${errorMessage}. Retrying...`);
@@ -2191,6 +2644,7 @@ var MCPClient = class _MCPClient {
2191
2644
  const errorMessage = lastError instanceof Error ? lastError.message : "Authentication failed";
2192
2645
  this.emitError(errorMessage, "auth");
2193
2646
  this.emitStateChange("FAILED");
2647
+ await this.deleteTransientSession();
2194
2648
  throw lastError;
2195
2649
  }
2196
2650
  }
@@ -2376,67 +2830,6 @@ var MCPClient = class _MCPClient {
2376
2830
  };
2377
2831
  return await this.client.request(request, types_js.ReadResourceResultSchema);
2378
2832
  }
2379
- /**
2380
- * Refreshes the OAuth access token using the refresh token
2381
- * Discovers OAuth metadata from server and exchanges refresh token for new access token
2382
- * @returns True if refresh was successful, false otherwise
2383
- */
2384
- async refreshToken() {
2385
- await this.initialize();
2386
- if (!this.oauthProvider) {
2387
- return false;
2388
- }
2389
- const tokens = await this.oauthProvider.tokens();
2390
- if (!tokens || !tokens.refresh_token) {
2391
- return false;
2392
- }
2393
- const clientInformation = await this.oauthProvider.clientInformation();
2394
- if (!clientInformation) {
2395
- return false;
2396
- }
2397
- try {
2398
- const resourceMetadata = await auth_js.discoverOAuthProtectedResourceMetadata(this.serverUrl);
2399
- const authServerUrl = resourceMetadata?.authorization_servers?.[0] || this.serverUrl;
2400
- const authMetadata = await auth_js.discoverAuthorizationServerMetadata(authServerUrl);
2401
- const newTokens = await auth_js.refreshAuthorization(authServerUrl, {
2402
- metadata: authMetadata,
2403
- clientInformation,
2404
- refreshToken: tokens.refresh_token
2405
- });
2406
- await this.oauthProvider.saveTokens(newTokens);
2407
- return true;
2408
- } catch (error) {
2409
- console.error("[OAuth] Token refresh failed:", error);
2410
- if (isInvalidRefreshTokenError(error)) {
2411
- try {
2412
- await this.oauthProvider.invalidateCredentials?.("tokens");
2413
- this.emitProgress("OAuth refresh token is invalid; requesting reauthorization...");
2414
- } catch (invalidateError) {
2415
- console.warn("[OAuth] Failed to invalidate stale refresh token credentials:", invalidateError);
2416
- }
2417
- }
2418
- return false;
2419
- }
2420
- }
2421
- /**
2422
- * Ensures OAuth tokens are valid, refreshing them if expired
2423
- * Called automatically by connect() - rarely needs to be called manually
2424
- * @returns True if valid tokens are available, false otherwise
2425
- */
2426
- async getValidTokens() {
2427
- await this.initialize();
2428
- if (!this.oauthProvider) {
2429
- return false;
2430
- }
2431
- const tokens = await this.oauthProvider.tokens();
2432
- if (!tokens) {
2433
- return false;
2434
- }
2435
- if (this.oauthProvider.isTokenExpired()) {
2436
- return await this.refreshToken();
2437
- }
2438
- return true;
2439
- }
2440
2833
  /**
2441
2834
  * Reconnects to MCP server using existing OAuth provider from Redis
2442
2835
  * Used for session restoration in serverless environments
@@ -2566,68 +2959,6 @@ var MCPClient = class _MCPClient {
2566
2959
  getSessionId() {
2567
2960
  return this.sessionId;
2568
2961
  }
2569
- /**
2570
- * Gets MCP server configuration for all active user sessions
2571
- * Loads sessions from Redis, validates OAuth tokens, refreshes if expired
2572
- * Returns ready-to-use configuration with valid auth headers
2573
- * @param userId - User ID to fetch sessions for
2574
- * @returns Object keyed by sanitized server labels containing transport, url, headers, etc.
2575
- * @static
2576
- */
2577
- static async getMcpServerConfig(userId) {
2578
- const mcpConfig = {};
2579
- const sessionList = await sessions.list(userId);
2580
- await Promise.all(
2581
- sessionList.map(async (sessionData) => {
2582
- const { sessionId } = sessionData;
2583
- try {
2584
- if (!sessionData.serverId || !sessionData.transportType || !sessionData.serverUrl || !sessionData.callbackUrl) {
2585
- await sessions.delete(userId, sessionId);
2586
- return;
2587
- }
2588
- let headers;
2589
- try {
2590
- const client = new _MCPClient({
2591
- userId,
2592
- sessionId,
2593
- serverId: sessionData.serverId,
2594
- serverUrl: sessionData.serverUrl,
2595
- callbackUrl: sessionData.callbackUrl,
2596
- serverName: sessionData.serverName,
2597
- transportType: sessionData.transportType,
2598
- headers: sessionData.headers
2599
- });
2600
- await client.initialize();
2601
- const hasValidTokens = await client.getValidTokens();
2602
- if (hasValidTokens && client.oauthProvider) {
2603
- const tokens = await client.oauthProvider.tokens();
2604
- if (tokens?.access_token) {
2605
- headers = { Authorization: `Bearer ${tokens.access_token}` };
2606
- }
2607
- }
2608
- } catch (error) {
2609
- console.warn(`[MCP] Failed to get OAuth tokens for ${sessionId}:`, error);
2610
- }
2611
- const label = sanitizeServerLabel(
2612
- sessionData.serverName || sessionData.serverId || "server"
2613
- );
2614
- mcpConfig[label] = {
2615
- transport: sessionData.transportType,
2616
- url: sessionData.serverUrl,
2617
- ...sessionData.serverName && {
2618
- serverName: sessionData.serverName,
2619
- serverLabel: label
2620
- },
2621
- ...headers && { headers }
2622
- };
2623
- } catch (error) {
2624
- await sessions.delete(userId, sessionId);
2625
- console.warn(`[MCP] Failed to process session ${sessionId}:`, error);
2626
- }
2627
- })
2628
- );
2629
- return mcpConfig;
2630
- }
2631
2962
  };
2632
2963
 
2633
2964
  // src/server/mcp/multi-session-client.ts
@@ -2663,18 +2994,13 @@ var MultiSessionClient = class {
2663
2994
  *
2664
2995
  * A session is considered connectable when:
2665
2996
  * - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
2666
- * - Its `active` flag is not explicitly `false` sessions with `active: false` are
2667
- * either mid-OAuth flow, auth-pending, or previously failed. We skip those here
2997
+ * - Its status is `active`. Pending sessions are skipped here
2668
2998
  * and let the OAuth flow complete separately before we try to reconnect them.
2669
- *
2670
- * Note: Sessions where `active` is `undefined` (legacy records) are included
2671
- * for backwards compatibility.
2672
2999
  */
2673
3000
  async getActiveSessions() {
2674
3001
  const sessionList = await sessions.list(this.userId);
2675
3002
  const valid = sessionList.filter(
2676
- (s) => s.serverId && s.serverUrl && s.callbackUrl && s.active !== false
2677
- // exclude OAuth-pending / failed sessions
3003
+ (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
2678
3004
  );
2679
3005
  return valid;
2680
3006
  }
@@ -2949,7 +3275,8 @@ var SSEConnectionManager = class {
2949
3275
  serverUrl: s.serverUrl,
2950
3276
  transport: s.transportType,
2951
3277
  createdAt: s.createdAt,
2952
- active: s.active !== false
3278
+ updatedAt: s.updatedAt ?? s.createdAt,
3279
+ status: s.status ?? "pending"
2953
3280
  }))
2954
3281
  };
2955
3282
  }
@@ -2965,7 +3292,7 @@ var SSEConnectionManager = class {
2965
3292
  (s) => s.serverId === serverId || s.serverUrl === serverUrl
2966
3293
  );
2967
3294
  if (duplicate) {
2968
- if (duplicate.active === false) {
3295
+ if (duplicate.status === "pending") {
2969
3296
  await this.getSession({ sessionId: duplicate.sessionId });
2970
3297
  return {
2971
3298
  sessionId: duplicate.sessionId,
@@ -3152,7 +3479,10 @@ var SSEConnectionManager = class {
3152
3479
  * Complete OAuth authorization flow
3153
3480
  */
3154
3481
  async finishAuth(params) {
3155
- const { sessionId, code } = params;
3482
+ const { code } = params;
3483
+ const oauthState = params.state;
3484
+ const parsedState = parseOAuthState(oauthState);
3485
+ const sessionId = parsedState?.sessionId || oauthState;
3156
3486
  const session = await sessions.get(this.userId, sessionId);
3157
3487
  if (!session) {
3158
3488
  throw new Error("Session not found");
@@ -3177,7 +3507,7 @@ var SSEConnectionManager = class {
3177
3507
  headers: session.headers
3178
3508
  });
3179
3509
  client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3180
- await client.finishAuth(code);
3510
+ await client.finishAuth(code, oauthState);
3181
3511
  this.clients.set(sessionId, client);
3182
3512
  const tools = await client.listTools();
3183
3513
  return { success: true, toolCount: tools.tools.length };