@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.mjs CHANGED
@@ -112,21 +112,7 @@ var init_redis = __esm({
112
112
  });
113
113
  }
114
114
  });
115
-
116
- // src/shared/constants.ts
117
- var SESSION_TTL_SECONDS = 43200;
118
- var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
119
- var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
120
- var REDIS_KEY_PREFIX = "mcp:session:";
121
- var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
122
- var DEFAULT_CLIENT_NAME = "MCP Assistant";
123
- var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
124
- var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
125
- var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
126
- var SOFTWARE_ID = "@mcp-ts";
127
- var SOFTWARE_VERSION = "1.3.4";
128
- var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
129
- var MCP_CLIENT_VERSION = "2.0";
115
+ var OAUTH_STATE_SEPARATOR = ".";
130
116
  var firstChar = customAlphabet(
131
117
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
132
118
  1
@@ -145,13 +131,98 @@ function sanitizeServerLabel(name) {
145
131
  function generateSessionId() {
146
132
  return firstChar() + rest();
147
133
  }
134
+ function formatOAuthState(nonce, sessionId) {
135
+ return `${nonce}${OAUTH_STATE_SEPARATOR}${sessionId}`;
136
+ }
137
+ function parseOAuthState(state) {
138
+ const separatorIndex = state.indexOf(OAUTH_STATE_SEPARATOR);
139
+ if (separatorIndex <= 0 || separatorIndex === state.length - 1) {
140
+ return void 0;
141
+ }
142
+ const nonce = state.slice(0, separatorIndex);
143
+ const sessionId = state.slice(separatorIndex + 1);
144
+ if (!nonce || !sessionId || sessionId.includes(OAUTH_STATE_SEPARATOR)) {
145
+ return void 0;
146
+ }
147
+ return { nonce, sessionId };
148
+ }
149
+
150
+ // src/shared/constants.ts
151
+ var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
152
+ var PENDING_SESSION_EXPIRATION_SECONDS = Math.floor(STATE_EXPIRATION_MS / 1e3);
153
+ var DORMANT_SESSION_EXPIRATION_MS = 30 * 24 * 60 * 60 * 1e3;
154
+ var DORMANT_SESSION_EXPIRATION_SECONDS = Math.floor(DORMANT_SESSION_EXPIRATION_MS / 1e3);
155
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
156
+ var REDIS_KEY_PREFIX = "mcp:session:";
157
+ var DEFAULT_CLIENT_NAME = "MCP Assistant";
158
+ var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
159
+ var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
160
+ var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
161
+ var SOFTWARE_ID = "@mcp-ts";
162
+ var SOFTWARE_VERSION = "2.3.4";
163
+ var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
164
+ var MCP_CLIENT_VERSION = "2.0";
165
+
166
+ // src/server/storage/session-lifecycle.ts
167
+ function resolveSessionExpiresAt(status = "pending", referenceTime = Date.now()) {
168
+ return status === "active" ? null : referenceTime + STATE_EXPIRATION_MS;
169
+ }
170
+ function resolveSessionRedisTtlSeconds(session) {
171
+ return session.status === "active" ? DORMANT_SESSION_EXPIRATION_SECONDS : Math.floor(STATE_EXPIRATION_MS / 1e3);
172
+ }
173
+ function normalizeNewSession(session, now = Date.now()) {
174
+ const createdAt = session.createdAt || now;
175
+ const updatedAt = session.updatedAt ?? createdAt;
176
+ const status = session.status ?? "pending";
177
+ return {
178
+ ...session,
179
+ status,
180
+ createdAt,
181
+ updatedAt,
182
+ expiresAt: resolveSessionExpiresAt(status, status === "active" ? updatedAt : createdAt)
183
+ };
184
+ }
185
+ function mergeSessionUpdate(current, data, now = Date.now()) {
186
+ const updatedAt = data.updatedAt ?? now;
187
+ const updated = {
188
+ ...current,
189
+ ...data,
190
+ updatedAt
191
+ };
192
+ const status = updated.status ?? "pending";
193
+ return {
194
+ ...updated,
195
+ status,
196
+ expiresAt: resolveSessionExpiresAt(status, updatedAt)
197
+ };
198
+ }
199
+ function normalizeStoredSession(session) {
200
+ const createdAt = session.createdAt || Date.now();
201
+ const updatedAt = session.updatedAt ?? createdAt;
202
+ const status = session.status ?? "pending";
203
+ const expiresAt = status === "active" ? null : session.expiresAt ?? resolveSessionExpiresAt(status, createdAt);
204
+ return {
205
+ ...session,
206
+ status,
207
+ createdAt,
208
+ updatedAt,
209
+ expiresAt
210
+ };
211
+ }
212
+ function isSessionExpired(session, now = Date.now()) {
213
+ const hydrated = normalizeStoredSession(session);
214
+ if (hydrated.status === "active") {
215
+ return hydrated.updatedAt !== void 0 && hydrated.updatedAt < now - DORMANT_SESSION_EXPIRATION_MS;
216
+ }
217
+ return typeof hydrated.expiresAt === "number" && hydrated.expiresAt < now;
218
+ }
148
219
 
149
220
  // src/server/storage/redis-backend.ts
150
221
  var RedisStorageBackend = class {
151
222
  constructor(redis2) {
152
223
  this.redis = redis2;
153
- __publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
154
224
  __publicField(this, "KEY_PREFIX", "mcp:session:");
225
+ __publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
155
226
  __publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
156
227
  __publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
157
228
  }
@@ -170,6 +241,9 @@ var RedisStorageBackend = class {
170
241
  getSessionKey(userId, sessionId) {
171
242
  return `${this.KEY_PREFIX}${userId}:${sessionId}`;
172
243
  }
244
+ getCredentialsKey(userId, sessionId) {
245
+ return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
246
+ }
173
247
  /**
174
248
  * Generates Redis key for tracking all sessions for a user
175
249
  * @private
@@ -207,15 +281,16 @@ var RedisStorageBackend = class {
207
281
  generateSessionId() {
208
282
  return generateSessionId();
209
283
  }
210
- async create(session, ttl) {
284
+ async create(session) {
211
285
  const { sessionId, userId } = session;
212
286
  if (!sessionId || !userId) throw new Error("userId and sessionId required");
213
287
  const sessionKey = this.getSessionKey(userId, sessionId);
214
288
  const userIdKey = this.getUserIdKey(userId);
215
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
289
+ const sessionWithLifecycle = normalizeNewSession(session);
290
+ const effectiveTtl = resolveSessionRedisTtlSeconds(sessionWithLifecycle);
216
291
  const result = await this.redis.set(
217
292
  sessionKey,
218
- JSON.stringify(session),
293
+ JSON.stringify(sessionWithLifecycle),
219
294
  "EX",
220
295
  effectiveTtl,
221
296
  "NX"
@@ -225,9 +300,8 @@ var RedisStorageBackend = class {
225
300
  }
226
301
  await this.redis.sadd(userIdKey, sessionId);
227
302
  }
228
- async update(userId, sessionId, data, ttl) {
303
+ async update(userId, sessionId, data) {
229
304
  const sessionKey = this.getSessionKey(userId, sessionId);
230
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
231
305
  const script = `
232
306
  local currentStr = redis.call("GET", KEYS[1])
233
307
  if not currentStr then
@@ -235,26 +309,47 @@ var RedisStorageBackend = class {
235
309
  end
236
310
 
237
311
  local current = cjson.decode(currentStr)
238
- local updates = cjson.decode(ARGV[1])
239
-
240
- for k,v in pairs(updates) do
241
- current[k] = v
242
- end
312
+ local updated = cjson.decode(ARGV[1])
243
313
 
244
- redis.call("SET", KEYS[1], cjson.encode(current), "EX", ARGV[2])
314
+ redis.call("SET", KEYS[1], cjson.encode(updated), "EX", ARGV[2])
245
315
  return 1
246
316
  `;
317
+ const current = await this.get(userId, sessionId);
318
+ if (!current) {
319
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
320
+ }
321
+ const updated = mergeSessionUpdate(current, data);
322
+ const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
247
323
  const result = await this.redis.eval(
248
324
  script,
249
325
  1,
250
326
  sessionKey,
251
- JSON.stringify(data),
327
+ JSON.stringify(updated),
252
328
  effectiveTtl
253
329
  );
254
330
  if (result === 0) {
255
331
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
256
332
  }
257
333
  }
334
+ async patchCredentials(userId, sessionId, data) {
335
+ const sessionKey = this.getSessionKey(userId, sessionId);
336
+ const credentialsKey = this.getCredentialsKey(userId, sessionId);
337
+ const sessionExists = await this.redis.exists(sessionKey);
338
+ if (!sessionExists) {
339
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
340
+ }
341
+ const session = await this.get(userId, sessionId);
342
+ const currentTtl = await this.redis.ttl(sessionKey);
343
+ const effectiveTtl = currentTtl > 0 && session ? currentTtl : resolveSessionRedisTtlSeconds(session ?? { status: "pending" });
344
+ const currentStr = await this.redis.get(credentialsKey);
345
+ const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
346
+ const credentials = { ...current, ...data, sessionId, userId };
347
+ if (effectiveTtl > 0) {
348
+ await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
349
+ } else {
350
+ await this.redis.set(credentialsKey, JSON.stringify(credentials));
351
+ }
352
+ }
258
353
  async get(userId, sessionId) {
259
354
  try {
260
355
  const sessionKey = this.getSessionKey(userId, sessionId);
@@ -262,13 +357,28 @@ var RedisStorageBackend = class {
262
357
  if (!sessionDataStr) {
263
358
  return null;
264
359
  }
265
- const Session = JSON.parse(sessionDataStr);
266
- return Session;
360
+ const session = JSON.parse(sessionDataStr);
361
+ return normalizeStoredSession(session);
267
362
  } catch (error) {
268
363
  console.error("[RedisStorageBackend] Failed to get session:", error);
269
364
  return null;
270
365
  }
271
366
  }
367
+ async getCredentials(userId, sessionId) {
368
+ const session = await this.get(userId, sessionId);
369
+ if (!session) return null;
370
+ const credentialsStr = await this.redis.get(this.getCredentialsKey(userId, sessionId));
371
+ return credentialsStr ? JSON.parse(credentialsStr) : { sessionId, userId };
372
+ }
373
+ async clearCredentials(userId, sessionId) {
374
+ await this.patchCredentials(userId, sessionId, {
375
+ clientInformation: null,
376
+ tokens: null,
377
+ codeVerifier: null,
378
+ clientId: null,
379
+ oauthState: null
380
+ });
381
+ }
272
382
  async listIds(userId) {
273
383
  const sessions2 = await this.list(userId);
274
384
  return sessions2.map((session) => session.sessionId);
@@ -288,7 +398,7 @@ var RedisStorageBackend = class {
288
398
  if (staleSessionIds.length > 0) {
289
399
  await this.redis.srem(userIdKey, ...staleSessionIds);
290
400
  }
291
- return results.filter((session) => session !== null);
401
+ return results.filter((session) => session !== null).map((session) => normalizeStoredSession(session));
292
402
  } catch (error) {
293
403
  console.error(`[RedisStorageBackend] Failed to get session data for ${userId}:`, error);
294
404
  return [];
@@ -297,9 +407,10 @@ var RedisStorageBackend = class {
297
407
  async delete(userId, sessionId) {
298
408
  try {
299
409
  const sessionKey = this.getSessionKey(userId, sessionId);
410
+ const credentialsKey = this.getCredentialsKey(userId, sessionId);
300
411
  const userIdKey = this.getUserIdKey(userId);
301
412
  await this.redis.srem(userIdKey, sessionId);
302
- await this.redis.del(sessionKey);
413
+ await this.redis.del(sessionKey, credentialsKey);
303
414
  } catch (error) {
304
415
  console.error("[RedisStorageBackend] Failed to remove session:", error);
305
416
  }
@@ -314,7 +425,7 @@ var RedisStorageBackend = class {
314
425
  return null;
315
426
  }
316
427
  try {
317
- return JSON.parse(data).sessionId;
428
+ return normalizeStoredSession(JSON.parse(data)).sessionId;
318
429
  } catch (error) {
319
430
  console.error("[RedisStorageBackend] Failed to parse session while listing all session IDs:", error);
320
431
  return null;
@@ -330,8 +441,9 @@ var RedisStorageBackend = class {
330
441
  async clearAll() {
331
442
  try {
332
443
  const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
444
+ const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
333
445
  const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
334
- const allKeys = [...keys, ...userIdKeys];
446
+ const allKeys = [...keys, ...credentialKeys, ...userIdKeys];
335
447
  if (allKeys.length > 0) {
336
448
  await this.redis.del(...allKeys);
337
449
  }
@@ -355,6 +467,7 @@ var RedisStorageBackend = class {
355
467
  const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
356
468
  if (staleSessionIds.length > 0) {
357
469
  await this.redis.srem(userIdKey, ...staleSessionIds);
470
+ await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
358
471
  }
359
472
  const remainingCount = await this.redis.scard(userIdKey);
360
473
  if (remainingCount === 0) {
@@ -379,6 +492,7 @@ var MemoryStorageBackend = class {
379
492
  constructor() {
380
493
  // Map<userId:sessionId, Session>
381
494
  __publicField(this, "sessions", /* @__PURE__ */ new Map());
495
+ __publicField(this, "credentials", /* @__PURE__ */ new Map());
382
496
  // Map<userId, Set<sessionId>>
383
497
  __publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
384
498
  }
@@ -391,36 +505,55 @@ var MemoryStorageBackend = class {
391
505
  generateSessionId() {
392
506
  return generateSessionId();
393
507
  }
394
- async create(session, ttl) {
508
+ async create(session) {
395
509
  const { sessionId, userId } = session;
396
510
  if (!sessionId || !userId) throw new Error("userId and sessionId required");
397
511
  const sessionKey = this.getSessionKey(userId, sessionId);
398
512
  if (this.sessions.has(sessionKey)) {
399
513
  throw new Error(`Session ${sessionId} already exists`);
400
514
  }
401
- this.sessions.set(sessionKey, session);
515
+ this.sessions.set(sessionKey, normalizeNewSession(session));
402
516
  if (!this.userIdSessions.has(userId)) {
403
517
  this.userIdSessions.set(userId, /* @__PURE__ */ new Set());
404
518
  }
405
519
  this.userIdSessions.get(userId).add(sessionId);
406
520
  }
407
- async update(userId, sessionId, data, ttl) {
521
+ async update(userId, sessionId, data) {
408
522
  if (!userId || !sessionId) throw new Error("userId and sessionId required");
409
523
  const sessionKey = this.getSessionKey(userId, sessionId);
410
524
  const current = this.sessions.get(sessionKey);
411
525
  if (!current) {
412
526
  throw new Error(`Session ${sessionId} not found`);
413
527
  }
414
- const updated = {
415
- ...current,
416
- ...data
417
- };
528
+ const updated = mergeSessionUpdate(current, data);
418
529
  this.sessions.set(sessionKey, updated);
419
530
  }
531
+ async patchCredentials(userId, sessionId, data) {
532
+ const sessionKey = this.getSessionKey(userId, sessionId);
533
+ if (!this.sessions.has(sessionKey)) {
534
+ throw new Error(`Session ${sessionId} not found`);
535
+ }
536
+ const current = this.credentials.get(sessionKey) ?? { sessionId, userId };
537
+ this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
538
+ }
420
539
  async get(userId, sessionId) {
421
540
  const sessionKey = this.getSessionKey(userId, sessionId);
422
541
  return this.sessions.get(sessionKey) || null;
423
542
  }
543
+ async getCredentials(userId, sessionId) {
544
+ const sessionKey = this.getSessionKey(userId, sessionId);
545
+ if (!this.sessions.has(sessionKey)) return null;
546
+ return this.credentials.get(sessionKey) ?? { sessionId, userId };
547
+ }
548
+ async clearCredentials(userId, sessionId) {
549
+ await this.patchCredentials(userId, sessionId, {
550
+ clientInformation: null,
551
+ tokens: null,
552
+ codeVerifier: null,
553
+ clientId: null,
554
+ oauthState: null
555
+ });
556
+ }
424
557
  async listIds(userId) {
425
558
  const set = this.userIdSessions.get(userId);
426
559
  return set ? Array.from(set) : [];
@@ -440,6 +573,7 @@ var MemoryStorageBackend = class {
440
573
  async delete(userId, sessionId) {
441
574
  const sessionKey = this.getSessionKey(userId, sessionId);
442
575
  this.sessions.delete(sessionKey);
576
+ this.credentials.delete(sessionKey);
443
577
  const set = this.userIdSessions.get(userId);
444
578
  if (set) {
445
579
  set.delete(sessionId);
@@ -453,9 +587,22 @@ var MemoryStorageBackend = class {
453
587
  }
454
588
  async clearAll() {
455
589
  this.sessions.clear();
590
+ this.credentials.clear();
456
591
  this.userIdSessions.clear();
457
592
  }
458
593
  async cleanupExpired() {
594
+ for (const [key, session] of this.sessions.entries()) {
595
+ if (!isSessionExpired(session)) continue;
596
+ this.sessions.delete(key);
597
+ this.credentials.delete(key);
598
+ const set = this.userIdSessions.get(session.userId);
599
+ if (set) {
600
+ set.delete(session.sessionId);
601
+ if (set.size === 0) {
602
+ this.userIdSessions.delete(session.userId);
603
+ }
604
+ }
605
+ }
459
606
  }
460
607
  async disconnect() {
461
608
  }
@@ -467,6 +614,7 @@ var FileStorageBackend = class {
467
614
  constructor(options = {}) {
468
615
  __publicField(this, "filePath");
469
616
  __publicField(this, "memoryCache", null);
617
+ __publicField(this, "credentialsCache", null);
470
618
  __publicField(this, "initialized", false);
471
619
  this.filePath = options.path || "./sessions.json";
472
620
  }
@@ -481,14 +629,22 @@ var FileStorageBackend = class {
481
629
  const data = await promises.readFile(this.filePath, "utf-8");
482
630
  const json = JSON.parse(data);
483
631
  this.memoryCache = /* @__PURE__ */ new Map();
484
- if (Array.isArray(json)) {
485
- json.forEach((s) => {
486
- this.memoryCache.set(this.getSessionKey(s.userId || "unknown", s.sessionId), s);
632
+ this.credentialsCache = /* @__PURE__ */ new Map();
633
+ if (Array.isArray(json.sessions)) {
634
+ json.sessions.forEach((s) => {
635
+ const session = normalizeStoredSession(s);
636
+ this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
637
+ });
638
+ }
639
+ if (Array.isArray(json.credentials)) {
640
+ json.credentials.forEach((c) => {
641
+ this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
487
642
  });
488
643
  }
489
644
  } catch (error) {
490
645
  if (error.code === "ENOENT") {
491
646
  this.memoryCache = /* @__PURE__ */ new Map();
647
+ this.credentialsCache = /* @__PURE__ */ new Map();
492
648
  await this.flush();
493
649
  } else {
494
650
  console.error("[FileStorage] Failed to load sessions:", error);
@@ -502,9 +658,11 @@ var FileStorageBackend = class {
502
658
  if (!this.initialized) await this.init();
503
659
  }
504
660
  async flush() {
505
- if (!this.memoryCache) return;
506
- const sessions2 = Array.from(this.memoryCache.values());
507
- await promises.writeFile(this.filePath, JSON.stringify(sessions2, null, 2), "utf-8");
661
+ if (!this.memoryCache || !this.credentialsCache) return;
662
+ await promises.writeFile(this.filePath, JSON.stringify({
663
+ sessions: Array.from(this.memoryCache.values()),
664
+ credentials: Array.from(this.credentialsCache.values())
665
+ }, null, 2), "utf-8");
508
666
  }
509
667
  getSessionKey(userId, sessionId) {
510
668
  return `${userId}:${sessionId}`;
@@ -512,7 +670,7 @@ var FileStorageBackend = class {
512
670
  generateSessionId() {
513
671
  return generateSessionId();
514
672
  }
515
- async create(session, ttl) {
673
+ async create(session) {
516
674
  await this.ensureInitialized();
517
675
  const { sessionId, userId } = session;
518
676
  if (!sessionId || !userId) throw new Error("userId and sessionId required");
@@ -520,10 +678,10 @@ var FileStorageBackend = class {
520
678
  if (this.memoryCache.has(sessionKey)) {
521
679
  throw new Error(`Session ${sessionId} already exists`);
522
680
  }
523
- this.memoryCache.set(sessionKey, session);
681
+ this.memoryCache.set(sessionKey, normalizeNewSession(session));
524
682
  await this.flush();
525
683
  }
526
- async update(userId, sessionId, data, ttl) {
684
+ async update(userId, sessionId, data) {
527
685
  await this.ensureInitialized();
528
686
  if (!userId || !sessionId) throw new Error("userId and sessionId required");
529
687
  const sessionKey = this.getSessionKey(userId, sessionId);
@@ -531,18 +689,40 @@ var FileStorageBackend = class {
531
689
  if (!current) {
532
690
  throw new Error(`Session ${sessionId} not found`);
533
691
  }
534
- const updated = {
535
- ...current,
536
- ...data
537
- };
692
+ const updated = mergeSessionUpdate(current, data);
538
693
  this.memoryCache.set(sessionKey, updated);
539
694
  await this.flush();
540
695
  }
696
+ async patchCredentials(userId, sessionId, data) {
697
+ await this.ensureInitialized();
698
+ const sessionKey = this.getSessionKey(userId, sessionId);
699
+ if (!this.memoryCache.has(sessionKey)) {
700
+ throw new Error(`Session ${sessionId} not found`);
701
+ }
702
+ const current = this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
703
+ this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
704
+ await this.flush();
705
+ }
541
706
  async get(userId, sessionId) {
542
707
  await this.ensureInitialized();
543
708
  const sessionKey = this.getSessionKey(userId, sessionId);
544
709
  return this.memoryCache.get(sessionKey) || null;
545
710
  }
711
+ async getCredentials(userId, sessionId) {
712
+ await this.ensureInitialized();
713
+ const sessionKey = this.getSessionKey(userId, sessionId);
714
+ if (!this.memoryCache.has(sessionKey)) return null;
715
+ return this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
716
+ }
717
+ async clearCredentials(userId, sessionId) {
718
+ await this.patchCredentials(userId, sessionId, {
719
+ clientInformation: null,
720
+ tokens: null,
721
+ codeVerifier: null,
722
+ clientId: null,
723
+ oauthState: null
724
+ });
725
+ }
546
726
  async list(userId) {
547
727
  await this.ensureInitialized();
548
728
  return Array.from(this.memoryCache.values()).filter((s) => s.userId === userId);
@@ -554,7 +734,9 @@ var FileStorageBackend = class {
554
734
  async delete(userId, sessionId) {
555
735
  await this.ensureInitialized();
556
736
  const sessionKey = this.getSessionKey(userId, sessionId);
557
- if (this.memoryCache.delete(sessionKey)) {
737
+ const deleted = this.memoryCache.delete(sessionKey);
738
+ this.credentialsCache.delete(sessionKey);
739
+ if (deleted) {
558
740
  await this.flush();
559
741
  }
560
742
  }
@@ -565,10 +747,21 @@ var FileStorageBackend = class {
565
747
  async clearAll() {
566
748
  await this.ensureInitialized();
567
749
  this.memoryCache.clear();
750
+ this.credentialsCache.clear();
568
751
  await this.flush();
569
752
  }
570
753
  async cleanupExpired() {
571
754
  await this.ensureInitialized();
755
+ let changed = false;
756
+ for (const [key, session] of this.memoryCache.entries()) {
757
+ if (!isSessionExpired(session)) continue;
758
+ this.memoryCache.delete(key);
759
+ this.credentialsCache.delete(key);
760
+ changed = true;
761
+ }
762
+ if (changed) {
763
+ await this.flush();
764
+ }
572
765
  }
573
766
  async disconnect() {
574
767
  }
@@ -577,10 +770,12 @@ var SqliteStorage = class {
577
770
  constructor(options = {}) {
578
771
  __publicField(this, "db", null);
579
772
  __publicField(this, "table");
773
+ __publicField(this, "credentialsTable");
580
774
  __publicField(this, "initialized", false);
581
775
  __publicField(this, "dbPath");
582
776
  this.dbPath = options.path || "./sessions.db";
583
777
  this.table = options.table || "mcp_sessions";
778
+ this.credentialsTable = `${this.table}_credentials`;
584
779
  }
585
780
  async init() {
586
781
  if (this.initialized) return;
@@ -591,6 +786,7 @@ var SqliteStorage = class {
591
786
  fs2.mkdirSync(dir, { recursive: true });
592
787
  }
593
788
  this.db = new DatabaseConstructor(this.dbPath);
789
+ this.db.pragma("foreign_keys = ON");
594
790
  this.db.exec(`
595
791
  CREATE TABLE IF NOT EXISTS ${this.table} (
596
792
  sessionId TEXT PRIMARY KEY,
@@ -599,6 +795,13 @@ var SqliteStorage = class {
599
795
  expiresAt INTEGER
600
796
  );
601
797
  CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
798
+ CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
799
+ sessionId TEXT NOT NULL,
800
+ userId TEXT NOT NULL,
801
+ data TEXT NOT NULL,
802
+ PRIMARY KEY (userId, sessionId),
803
+ FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
804
+ );
602
805
  `);
603
806
  this.initialized = true;
604
807
  console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
@@ -619,18 +822,18 @@ var SqliteStorage = class {
619
822
  generateSessionId() {
620
823
  return generateSessionId();
621
824
  }
622
- async create(session, ttl) {
825
+ async create(session) {
623
826
  this.ensureInitialized();
624
827
  const { sessionId, userId } = session;
625
828
  if (!sessionId || !userId) {
626
829
  throw new Error("userId and sessionId required");
627
830
  }
628
- const expiresAt = ttl ? Date.now() + ttl * 1e3 : null;
831
+ const sessionWithLifecycle = normalizeNewSession(session);
629
832
  try {
630
833
  const stmt = this.db.prepare(
631
834
  `INSERT INTO ${this.table} (sessionId, userId, data, expiresAt) VALUES (?, ?, ?, ?)`
632
835
  );
633
- stmt.run(sessionId, userId, JSON.stringify(session), expiresAt);
836
+ stmt.run(sessionId, userId, JSON.stringify(sessionWithLifecycle), sessionWithLifecycle.expiresAt ?? null);
634
837
  } catch (error) {
635
838
  if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
636
839
  throw new Error(`Session ${sessionId} already exists`);
@@ -638,7 +841,7 @@ var SqliteStorage = class {
638
841
  throw error;
639
842
  }
640
843
  }
641
- async update(userId, sessionId, data, ttl) {
844
+ async update(userId, sessionId, data) {
642
845
  this.ensureInitialized();
643
846
  if (!sessionId || !userId) {
644
847
  throw new Error("userId and sessionId required");
@@ -647,12 +850,25 @@ var SqliteStorage = class {
647
850
  if (!currentSession) {
648
851
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
649
852
  }
650
- const updatedSession = { ...currentSession, ...data };
651
- const expiresAt = ttl ? Date.now() + ttl * 1e3 : null;
853
+ const updatedSession = mergeSessionUpdate(currentSession, data);
652
854
  const stmt = this.db.prepare(
653
855
  `UPDATE ${this.table} SET data = ?, expiresAt = ? WHERE sessionId = ? AND userId = ?`
654
856
  );
655
- stmt.run(JSON.stringify(updatedSession), expiresAt, sessionId, userId);
857
+ stmt.run(JSON.stringify(updatedSession), updatedSession.expiresAt ?? null, sessionId, userId);
858
+ }
859
+ async patchCredentials(userId, sessionId, data) {
860
+ this.ensureInitialized();
861
+ if (!await this.get(userId, sessionId)) {
862
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
863
+ }
864
+ const current = await this.getCredentials(userId, sessionId) ?? { sessionId, userId };
865
+ const credentials = { ...current, ...data, sessionId, userId };
866
+ const stmt = this.db.prepare(
867
+ `INSERT INTO ${this.credentialsTable} (sessionId, userId, data)
868
+ VALUES (?, ?, ?)
869
+ ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
870
+ );
871
+ stmt.run(sessionId, userId, JSON.stringify(credentials));
656
872
  }
657
873
  async get(userId, sessionId) {
658
874
  this.ensureInitialized();
@@ -661,7 +877,25 @@ var SqliteStorage = class {
661
877
  );
662
878
  const row = stmt.get(sessionId, userId);
663
879
  if (!row) return null;
664
- return JSON.parse(row.data);
880
+ return normalizeStoredSession(JSON.parse(row.data));
881
+ }
882
+ async getCredentials(userId, sessionId) {
883
+ this.ensureInitialized();
884
+ if (!await this.get(userId, sessionId)) return null;
885
+ const stmt = this.db.prepare(
886
+ `SELECT data FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
887
+ );
888
+ const row = stmt.get(sessionId, userId);
889
+ return row ? JSON.parse(row.data) : { sessionId, userId };
890
+ }
891
+ async clearCredentials(userId, sessionId) {
892
+ await this.patchCredentials(userId, sessionId, {
893
+ clientInformation: null,
894
+ tokens: null,
895
+ codeVerifier: null,
896
+ clientId: null,
897
+ oauthState: null
898
+ });
665
899
  }
666
900
  async list(userId) {
667
901
  this.ensureInitialized();
@@ -669,7 +903,7 @@ var SqliteStorage = class {
669
903
  `SELECT data FROM ${this.table} WHERE userId = ?`
670
904
  );
671
905
  const rows = stmt.all(userId);
672
- return rows.map((row) => JSON.parse(row.data));
906
+ return rows.map((row) => normalizeStoredSession(JSON.parse(row.data)));
673
907
  }
674
908
  async listIds(userId) {
675
909
  this.ensureInitialized();
@@ -684,6 +918,9 @@ var SqliteStorage = class {
684
918
  const stmt = this.db.prepare(
685
919
  `DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
686
920
  );
921
+ this.db.prepare(
922
+ `DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
923
+ ).run(sessionId, userId);
687
924
  stmt.run(sessionId, userId);
688
925
  }
689
926
  async listAllIds() {
@@ -694,16 +931,28 @@ var SqliteStorage = class {
694
931
  }
695
932
  async clearAll() {
696
933
  this.ensureInitialized();
934
+ this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
697
935
  const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
698
936
  stmt.run();
699
937
  }
700
938
  async cleanupExpired() {
701
939
  this.ensureInitialized();
702
- const now = Date.now();
703
- const stmt = this.db.prepare(
704
- `DELETE FROM ${this.table} WHERE expiresAt IS NOT NULL AND expiresAt < ?`
705
- );
706
- stmt.run(now);
940
+ const rows = this.db.prepare(`SELECT sessionId, userId, data FROM ${this.table}`).all();
941
+ const deleteStmt = this.db.prepare(`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`);
942
+ for (const row of rows) {
943
+ const session = normalizeStoredSession(JSON.parse(row.data));
944
+ if (isSessionExpired(session)) {
945
+ deleteStmt.run(row.sessionId, row.userId);
946
+ }
947
+ }
948
+ this.db.prepare(
949
+ `DELETE FROM ${this.credentialsTable}
950
+ WHERE NOT EXISTS (
951
+ SELECT 1 FROM ${this.table}
952
+ WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
953
+ AND ${this.table}.userId = ${this.credentialsTable}.userId
954
+ )`
955
+ ).run();
707
956
  }
708
957
  async disconnect() {
709
958
  if (this.db) {
@@ -782,19 +1031,21 @@ function decryptObject(data) {
782
1031
  var SupabaseStorageBackend = class {
783
1032
  constructor(supabase) {
784
1033
  this.supabase = supabase;
785
- __publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
786
1034
  }
787
1035
  async init() {
788
- const { error } = await this.supabase.from("mcp_sessions").select("session_id").limit(0);
789
- if (error) {
790
- if (error.code === "42P01") {
791
- throw new Error(
792
- '[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.'
793
- );
794
- }
795
- throw new Error(`[SupabaseStorage] Initialization check failed: ${error.message}`);
1036
+ await this.assertTable("mcp_sessions", "session_id");
1037
+ await this.assertTable("mcp_credentials", "session_id");
1038
+ console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
1039
+ }
1040
+ async assertTable(table, column) {
1041
+ const { error } = await this.supabase.from(table).select(column).limit(0);
1042
+ if (!error) return;
1043
+ if (error.code === "42P01") {
1044
+ throw new Error(
1045
+ `[SupabaseStorage] Table "${table}" not found in your database. Please run "npx mcp-ts supabase-init" to set up the required storage schema.`
1046
+ );
796
1047
  }
797
- console.log('[mcp-ts][Storage] Supabase: \u2713 "mcp_sessions" table verified.');
1048
+ throw new Error(`[SupabaseStorage] Initialization check failed for "${table}": ${error.message}`);
798
1049
  }
799
1050
  generateSessionId() {
800
1051
  return generateSessionId();
@@ -808,37 +1059,49 @@ var SupabaseStorageBackend = class {
808
1059
  transportType: row.transport_type,
809
1060
  callbackUrl: row.callback_url,
810
1061
  createdAt: new Date(row.created_at).getTime(),
1062
+ updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
1063
+ expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
811
1064
  userId: row.user_id,
812
1065
  headers: decryptObject(row.headers),
813
- active: row.active,
814
- clientInformation: row.client_information,
815
- tokens: decryptObject(row.tokens),
816
- codeVerifier: row.code_verifier,
817
- clientId: row.client_id
1066
+ authUrl: row.auth_url,
1067
+ status: row.status ?? "pending"
1068
+ };
1069
+ }
1070
+ mapRowToCredentials(row, userId, sessionId) {
1071
+ return {
1072
+ sessionId,
1073
+ userId,
1074
+ clientInformation: decryptObject(row?.client_information),
1075
+ tokens: decryptObject(row?.tokens),
1076
+ codeVerifier: decryptObject(row?.code_verifier),
1077
+ clientId: row?.client_id,
1078
+ oauthState: row?.oauth_state
818
1079
  };
819
1080
  }
820
- async create(session, ttl) {
1081
+ hasCredentialData(data) {
1082
+ return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
1083
+ }
1084
+ async create(session) {
821
1085
  const { sessionId, userId } = session;
822
1086
  if (!sessionId || !userId) throw new Error("userId and sessionId required");
823
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
824
- const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
1087
+ const status = session.status ?? "pending";
1088
+ const createdAt = new Date(session.createdAt || Date.now()).toISOString();
1089
+ const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
1090
+ const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
825
1091
  const { error } = await this.supabase.from("mcp_sessions").insert({
826
1092
  session_id: sessionId,
827
1093
  user_id: userId,
828
- // Maps user_id to userId to support RLS using auth.uid()
829
1094
  server_id: session.serverId,
830
1095
  server_name: session.serverName,
831
1096
  server_url: session.serverUrl,
832
1097
  transport_type: session.transportType,
833
1098
  callback_url: session.callbackUrl,
834
- created_at: new Date(session.createdAt || Date.now()).toISOString(),
1099
+ created_at: createdAt,
1100
+ updated_at: updatedAt,
835
1101
  headers: encryptObject(session.headers),
836
- active: session.active ?? false,
837
- client_information: session.clientInformation,
838
- tokens: encryptObject(session.tokens),
839
- code_verifier: session.codeVerifier,
840
- client_id: session.clientId,
841
- expires_at: expiresAt
1102
+ auth_url: session.authUrl ?? null,
1103
+ status,
1104
+ expires_at: expiresAt === null ? null : new Date(expiresAt).toISOString()
842
1105
  });
843
1106
  if (error) {
844
1107
  if (error.code === "23505") {
@@ -847,11 +1110,8 @@ var SupabaseStorageBackend = class {
847
1110
  throw new Error(`Failed to create session in Supabase: ${error.message}`);
848
1111
  }
849
1112
  }
850
- async update(userId, sessionId, data, ttl) {
851
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
852
- const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
1113
+ async update(userId, sessionId, data) {
853
1114
  const updateData = {
854
- expires_at: expiresAt,
855
1115
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
856
1116
  };
857
1117
  if ("serverId" in data) updateData.server_id = data.serverId;
@@ -859,20 +1119,50 @@ var SupabaseStorageBackend = class {
859
1119
  if ("serverUrl" in data) updateData.server_url = data.serverUrl;
860
1120
  if ("transportType" in data) updateData.transport_type = data.transportType;
861
1121
  if ("callbackUrl" in data) updateData.callback_url = data.callbackUrl;
862
- if ("active" in data) updateData.active = data.active;
1122
+ if ("status" in data) {
1123
+ const status = data.status ?? "pending";
1124
+ const expiresAt = resolveSessionExpiresAt(status);
1125
+ updateData.status = status;
1126
+ updateData.expires_at = expiresAt === null ? null : new Date(expiresAt).toISOString();
1127
+ }
863
1128
  if ("headers" in data) updateData.headers = encryptObject(data.headers);
864
- if ("clientInformation" in data) updateData.client_information = data.clientInformation;
865
- if ("tokens" in data) updateData.tokens = data.tokens === void 0 ? null : encryptObject(data.tokens);
866
- if ("codeVerifier" in data) updateData.code_verifier = data.codeVerifier;
867
- if ("clientId" in data) updateData.client_id = data.clientId;
868
- const { data: updatedRows, error } = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId).select("id");
869
- if (error) {
870
- throw new Error(`Failed to update session: ${error.message}`);
1129
+ if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
1130
+ const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
1131
+ let updatedRows = null;
1132
+ if (shouldUpdateSession) {
1133
+ const result = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId).select("id");
1134
+ if (result.error) {
1135
+ throw new Error(`Failed to update session: ${result.error.message}`);
1136
+ }
1137
+ updatedRows = result.data;
1138
+ } else {
1139
+ const result = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
1140
+ if (result.error) {
1141
+ throw new Error(`Failed to update session: ${result.error.message}`);
1142
+ }
1143
+ updatedRows = result.data;
871
1144
  }
872
1145
  if (!updatedRows || updatedRows.length === 0) {
873
1146
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
874
1147
  }
875
1148
  }
1149
+ async patchCredentials(userId, sessionId, data) {
1150
+ if (!this.hasCredentialData(data)) return;
1151
+ const row = {
1152
+ user_id: userId,
1153
+ session_id: sessionId,
1154
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1155
+ };
1156
+ if ("clientInformation" in data) row.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
1157
+ if ("tokens" in data) row.tokens = data.tokens == null ? null : encryptObject(data.tokens);
1158
+ if ("codeVerifier" in data) row.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
1159
+ if ("clientId" in data) row.client_id = data.clientId ?? null;
1160
+ if ("oauthState" in data) row.oauth_state = data.oauthState ?? null;
1161
+ const { error } = await this.supabase.from("mcp_credentials").upsert(row, { onConflict: "user_id,session_id" });
1162
+ if (error) {
1163
+ throw new Error(`Failed to update credentials: ${error.message}`);
1164
+ }
1165
+ }
876
1166
  async get(userId, sessionId) {
877
1167
  const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
878
1168
  if (error) {
@@ -882,6 +1172,21 @@ var SupabaseStorageBackend = class {
882
1172
  if (!data) return null;
883
1173
  return this.mapRowToSessionData(data);
884
1174
  }
1175
+ async getCredentials(userId, sessionId) {
1176
+ const { data, error } = await this.supabase.from("mcp_credentials").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
1177
+ if (error) {
1178
+ console.error("[SupabaseStorage] Failed to get credentials:", error);
1179
+ return null;
1180
+ }
1181
+ if (data) {
1182
+ return this.mapRowToCredentials(data, userId, sessionId);
1183
+ }
1184
+ const { data: sessionRows, error: sessionError } = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
1185
+ if (sessionError || !sessionRows || sessionRows.length === 0) {
1186
+ return null;
1187
+ }
1188
+ return { sessionId, userId };
1189
+ }
885
1190
  async list(userId) {
886
1191
  const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
887
1192
  if (error) {
@@ -890,6 +1195,12 @@ var SupabaseStorageBackend = class {
890
1195
  }
891
1196
  return data.map((row) => this.mapRowToSessionData(row));
892
1197
  }
1198
+ async clearCredentials(userId, sessionId) {
1199
+ const { error } = await this.supabase.from("mcp_credentials").delete().eq("user_id", userId).eq("session_id", sessionId);
1200
+ if (error) {
1201
+ throw new Error(`Failed to clear credentials: ${error.message}`);
1202
+ }
1203
+ }
893
1204
  async delete(userId, sessionId) {
894
1205
  const { error } = await this.supabase.from("mcp_sessions").delete().eq("user_id", userId).eq("session_id", sessionId);
895
1206
  if (error) {
@@ -913,15 +1224,24 @@ var SupabaseStorageBackend = class {
913
1224
  return data.map((row) => row.session_id);
914
1225
  }
915
1226
  async clearAll() {
1227
+ const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
1228
+ if (credentialsError) {
1229
+ console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
1230
+ }
916
1231
  const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
917
1232
  if (error) {
918
1233
  console.error("[SupabaseStorage] Failed to clear sessions:", error);
919
1234
  }
920
1235
  }
921
1236
  async cleanupExpired() {
922
- const { error } = await this.supabase.from("mcp_sessions").delete().lt("expires_at", (/* @__PURE__ */ new Date()).toISOString());
923
- if (error) {
924
- console.error("[SupabaseStorage] Failed to cleanup expired sessions:", error);
1237
+ 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());
1238
+ if (transientError) {
1239
+ console.error("[SupabaseStorage] Failed to cleanup expired inactive sessions:", transientError);
1240
+ }
1241
+ const dormantCutoff = new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString();
1242
+ const { error: dormantError } = await this.supabase.from("mcp_sessions").delete().eq("status", "active").lt("updated_at", dormantCutoff);
1243
+ if (dormantError) {
1244
+ console.error("[SupabaseStorage] Failed to cleanup dormant active sessions:", dormantError);
925
1245
  }
926
1246
  }
927
1247
  async disconnect() {
@@ -932,23 +1252,29 @@ var SupabaseStorageBackend = class {
932
1252
  var NeonStorageBackend = class {
933
1253
  constructor(sql, options = {}) {
934
1254
  this.sql = sql;
935
- __publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
936
1255
  __publicField(this, "tableName");
1256
+ __publicField(this, "credentialsTableName");
937
1257
  const schema = options.schema || "public";
938
1258
  const table = options.table || "mcp_sessions";
1259
+ const credentialsTable = options.credentialsTable || "mcp_credentials";
939
1260
  this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
1261
+ this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
940
1262
  }
941
1263
  async init() {
1264
+ await this.assertTable(this.tableName, "mcp_sessions");
1265
+ await this.assertTable(this.credentialsTableName, "mcp_credentials");
1266
+ console.log("[mcp-ts][Storage] Neon: storage tables verified.");
1267
+ }
1268
+ async assertTable(qualifiedName, displayName) {
942
1269
  const [{ exists } = { exists: null }] = await this.sql.query(
943
1270
  "SELECT to_regclass($1) AS exists",
944
- [this.tableName.replace(/"/g, "")]
1271
+ [qualifiedName.replace(/"/g, "")]
945
1272
  );
946
1273
  if (!exists) {
947
1274
  throw new Error(
948
- '[NeonStorage] Table "mcp_sessions" not found in your database. Please create it using the Neon storage guide in docs/storage-backends/neon.md.'
1275
+ `[NeonStorage] Table "${displayName}" not found in your database. Please create it using the Neon storage guide in docs/storage-backends/neon.md.`
949
1276
  );
950
1277
  }
951
- console.log('[mcp-ts][Storage] Neon: "mcp_sessions" table verified.');
952
1278
  }
953
1279
  generateSessionId() {
954
1280
  return generateSessionId();
@@ -968,20 +1294,35 @@ var NeonStorageBackend = class {
968
1294
  transportType: row.transport_type,
969
1295
  callbackUrl: row.callback_url,
970
1296
  createdAt: new Date(row.created_at).getTime(),
1297
+ updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
1298
+ expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
971
1299
  userId: row.user_id,
972
1300
  headers: decryptObject(row.headers),
973
- active: row.active ?? false,
974
- clientInformation: row.client_information,
1301
+ authUrl: row.auth_url ?? void 0,
1302
+ status: row.status ?? "pending"
1303
+ };
1304
+ }
1305
+ mapRowToCredentials(row, userId, sessionId) {
1306
+ return {
1307
+ sessionId,
1308
+ userId,
1309
+ clientInformation: decryptObject(row.client_information),
975
1310
  tokens: decryptObject(row.tokens),
976
- codeVerifier: row.code_verifier ?? void 0,
977
- clientId: row.client_id ?? void 0
1311
+ codeVerifier: decryptObject(row.code_verifier),
1312
+ clientId: row.client_id ?? void 0,
1313
+ oauthState: row.oauth_state
978
1314
  };
979
1315
  }
980
- async create(session, ttl) {
1316
+ hasCredentialData(data) {
1317
+ return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
1318
+ }
1319
+ async create(session) {
981
1320
  const { sessionId, userId } = session;
982
1321
  if (!sessionId || !userId) throw new Error("userId and sessionId required");
983
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
984
- const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
1322
+ const status = session.status ?? "pending";
1323
+ const createdAt = new Date(session.createdAt || Date.now()).toISOString();
1324
+ const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
1325
+ const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
985
1326
  try {
986
1327
  await this.sql.query(
987
1328
  `INSERT INTO ${this.tableName} (
@@ -993,16 +1334,14 @@ var NeonStorageBackend = class {
993
1334
  transport_type,
994
1335
  callback_url,
995
1336
  created_at,
1337
+ updated_at,
996
1338
  headers,
997
- active,
998
- client_information,
999
- tokens,
1000
- code_verifier,
1001
- client_id,
1339
+ auth_url,
1340
+ status,
1002
1341
  expires_at
1003
1342
  ) VALUES (
1004
1343
  $1, $2, $3, $4, $5, $6, $7, $8,
1005
- $9, $10, $11, $12, $13, $14, $15
1344
+ $9, $10, $11, $12, $13
1006
1345
  )`,
1007
1346
  [
1008
1347
  sessionId,
@@ -1012,14 +1351,12 @@ var NeonStorageBackend = class {
1012
1351
  session.serverUrl,
1013
1352
  session.transportType,
1014
1353
  session.callbackUrl,
1015
- new Date(session.createdAt || Date.now()).toISOString(),
1354
+ createdAt,
1355
+ updatedAt,
1016
1356
  encryptObject(session.headers),
1017
- session.active ?? false,
1018
- session.clientInformation,
1019
- encryptObject(session.tokens),
1020
- session.codeVerifier,
1021
- session.clientId,
1022
- expiresAt
1357
+ session.authUrl ?? null,
1358
+ status,
1359
+ expiresAt === null ? null : new Date(expiresAt).toISOString()
1023
1360
  ]
1024
1361
  );
1025
1362
  } catch (error) {
@@ -1029,52 +1366,86 @@ var NeonStorageBackend = class {
1029
1366
  throw new Error(`Failed to create session in Neon: ${error.message}`);
1030
1367
  }
1031
1368
  }
1032
- async update(userId, sessionId, data, ttl) {
1369
+ async update(userId, sessionId, data) {
1033
1370
  const currentSession = await this.get(userId, sessionId);
1034
1371
  if (!currentSession) {
1035
1372
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
1036
1373
  }
1037
1374
  const updatedSession = { ...currentSession, ...data };
1038
- const effectiveTtl = ttl ?? this.DEFAULT_TTL;
1039
- const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
1040
- const updatedRows = await this.sql.query(
1041
- `UPDATE ${this.tableName}
1042
- SET
1043
- server_id = $1,
1044
- server_name = $2,
1045
- server_url = $3,
1046
- transport_type = $4,
1047
- callback_url = $5,
1048
- active = $6,
1049
- headers = $7,
1050
- client_information = $8,
1051
- tokens = $9,
1052
- code_verifier = $10,
1053
- client_id = $11,
1054
- expires_at = $12,
1055
- updated_at = now()
1056
- WHERE user_id = $13 AND session_id = $14
1057
- RETURNING id`,
1375
+ const status = updatedSession.status ?? "pending";
1376
+ const expiresAt = resolveSessionExpiresAt(status);
1377
+ 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;
1378
+ if (shouldUpdateSession) {
1379
+ const updatedRows = await this.sql.query(
1380
+ `UPDATE ${this.tableName}
1381
+ SET
1382
+ server_id = $1,
1383
+ server_name = $2,
1384
+ server_url = $3,
1385
+ transport_type = $4,
1386
+ callback_url = $5,
1387
+ status = $6,
1388
+ headers = $7,
1389
+ auth_url = $8,
1390
+ expires_at = $9,
1391
+ updated_at = now()
1392
+ WHERE user_id = $10 AND session_id = $11
1393
+ RETURNING id`,
1394
+ [
1395
+ updatedSession.serverId,
1396
+ updatedSession.serverName,
1397
+ updatedSession.serverUrl,
1398
+ updatedSession.transportType,
1399
+ updatedSession.callbackUrl,
1400
+ status,
1401
+ encryptObject(updatedSession.headers),
1402
+ updatedSession.authUrl ?? null,
1403
+ expiresAt === null ? null : new Date(expiresAt).toISOString(),
1404
+ userId,
1405
+ sessionId
1406
+ ]
1407
+ );
1408
+ if (updatedRows.length === 0) {
1409
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
1410
+ }
1411
+ }
1412
+ }
1413
+ async patchCredentials(userId, sessionId, data) {
1414
+ if (!this.hasCredentialData(data)) return;
1415
+ await this.sql.query(
1416
+ `INSERT INTO ${this.credentialsTableName} (
1417
+ user_id,
1418
+ session_id,
1419
+ client_information,
1420
+ tokens,
1421
+ code_verifier,
1422
+ client_id,
1423
+ oauth_state,
1424
+ updated_at
1425
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, now())
1426
+ ON CONFLICT (user_id, session_id)
1427
+ DO UPDATE SET
1428
+ client_information = CASE WHEN $8 THEN EXCLUDED.client_information ELSE ${this.credentialsTableName}.client_information END,
1429
+ tokens = CASE WHEN $9 THEN EXCLUDED.tokens ELSE ${this.credentialsTableName}.tokens END,
1430
+ code_verifier = CASE WHEN $10 THEN EXCLUDED.code_verifier ELSE ${this.credentialsTableName}.code_verifier END,
1431
+ client_id = CASE WHEN $11 THEN EXCLUDED.client_id ELSE ${this.credentialsTableName}.client_id END,
1432
+ oauth_state = CASE WHEN $12 THEN EXCLUDED.oauth_state ELSE ${this.credentialsTableName}.oauth_state END,
1433
+ updated_at = now()`,
1058
1434
  [
1059
- updatedSession.serverId,
1060
- updatedSession.serverName,
1061
- updatedSession.serverUrl,
1062
- updatedSession.transportType,
1063
- updatedSession.callbackUrl,
1064
- updatedSession.active ?? false,
1065
- encryptObject(updatedSession.headers),
1066
- updatedSession.clientInformation,
1067
- updatedSession.tokens === void 0 ? null : encryptObject(updatedSession.tokens),
1068
- updatedSession.codeVerifier,
1069
- updatedSession.clientId,
1070
- expiresAt,
1071
1435
  userId,
1072
- sessionId
1436
+ sessionId,
1437
+ "clientInformation" in data ? data.clientInformation == null ? null : encryptObject(data.clientInformation) : null,
1438
+ "tokens" in data ? data.tokens == null ? null : encryptObject(data.tokens) : null,
1439
+ "codeVerifier" in data ? data.codeVerifier == null ? null : encryptObject(data.codeVerifier) : null,
1440
+ "clientId" in data ? data.clientId ?? null : null,
1441
+ "oauthState" in data ? data.oauthState ?? null : null,
1442
+ "clientInformation" in data,
1443
+ "tokens" in data,
1444
+ "codeVerifier" in data,
1445
+ "clientId" in data,
1446
+ "oauthState" in data
1073
1447
  ]
1074
1448
  );
1075
- if (updatedRows.length === 0) {
1076
- throw new Error(`Session ${sessionId} not found for userId ${userId}`);
1077
- }
1078
1449
  }
1079
1450
  async get(userId, sessionId) {
1080
1451
  try {
@@ -1082,12 +1453,32 @@ var NeonStorageBackend = class {
1082
1453
  `SELECT * FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1083
1454
  [userId, sessionId]
1084
1455
  );
1085
- return rows[0] ? this.mapRowToSessionData(rows[0]) : null;
1456
+ if (!rows[0]) return null;
1457
+ return this.mapRowToSessionData(rows[0]);
1086
1458
  } catch (error) {
1087
1459
  console.error("[NeonStorage] Failed to get session:", error);
1088
1460
  return null;
1089
1461
  }
1090
1462
  }
1463
+ async getCredentials(userId, sessionId) {
1464
+ try {
1465
+ const credentialRows = await this.sql.query(
1466
+ `SELECT * FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
1467
+ [userId, sessionId]
1468
+ );
1469
+ if (credentialRows[0]) {
1470
+ return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
1471
+ }
1472
+ const sessionRows = await this.sql.query(
1473
+ `SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1474
+ [userId, sessionId]
1475
+ );
1476
+ return sessionRows[0] ? { sessionId, userId } : null;
1477
+ } catch (error) {
1478
+ console.error("[NeonStorage] Failed to get credentials:", error);
1479
+ return null;
1480
+ }
1481
+ }
1091
1482
  async list(userId) {
1092
1483
  try {
1093
1484
  const rows = await this.sql.query(
@@ -1100,6 +1491,16 @@ var NeonStorageBackend = class {
1100
1491
  return [];
1101
1492
  }
1102
1493
  }
1494
+ async clearCredentials(userId, sessionId) {
1495
+ try {
1496
+ await this.sql.query(
1497
+ `DELETE FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
1498
+ [userId, sessionId]
1499
+ );
1500
+ } catch (error) {
1501
+ console.error("[NeonStorage] Failed to clear credentials:", error);
1502
+ }
1503
+ }
1103
1504
  async delete(userId, sessionId) {
1104
1505
  try {
1105
1506
  await this.sql.query(
@@ -1135,6 +1536,7 @@ var NeonStorageBackend = class {
1135
1536
  }
1136
1537
  async clearAll() {
1137
1538
  try {
1539
+ await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
1138
1540
  await this.sql.query(`DELETE FROM ${this.tableName}`);
1139
1541
  } catch (error) {
1140
1542
  console.error("[NeonStorage] Failed to clear sessions:", error);
@@ -1143,9 +1545,17 @@ var NeonStorageBackend = class {
1143
1545
  async cleanupExpired() {
1144
1546
  try {
1145
1547
  await this.sql.query(
1146
- `DELETE FROM ${this.tableName} WHERE expires_at < $1`,
1548
+ `DELETE FROM ${this.tableName}
1549
+ WHERE expires_at IS NOT NULL
1550
+ AND expires_at < $1
1551
+ AND status <> 'active'`,
1147
1552
  [(/* @__PURE__ */ new Date()).toISOString()]
1148
1553
  );
1554
+ await this.sql.query(
1555
+ `DELETE FROM ${this.tableName}
1556
+ WHERE status = 'active' AND updated_at < $1`,
1557
+ [new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString()]
1558
+ );
1149
1559
  } catch (error) {
1150
1560
  console.error("[NeonStorage] Failed to cleanup expired sessions:", error);
1151
1561
  }
@@ -1192,26 +1602,24 @@ function emitSessionMutation(event) {
1192
1602
  function createSessionMutationEvent(prop, args) {
1193
1603
  const timestamp = Date.now();
1194
1604
  if (prop === "create") {
1195
- const [session, ttl] = args;
1605
+ const [session] = args;
1196
1606
  if (!session?.userId || !session?.sessionId) return null;
1197
1607
  return {
1198
1608
  type: "create",
1199
1609
  userId: session.userId,
1200
1610
  sessionId: session.sessionId,
1201
1611
  session,
1202
- ttl,
1203
1612
  timestamp
1204
1613
  };
1205
1614
  }
1206
1615
  if (prop === "update") {
1207
- const [userId, sessionId, patch, ttl] = args;
1616
+ const [userId, sessionId, patch] = args;
1208
1617
  if (!userId || !sessionId) return null;
1209
1618
  return {
1210
1619
  type: "update",
1211
1620
  userId,
1212
1621
  sessionId,
1213
1622
  patch,
1214
- ttl,
1215
1623
  timestamp
1216
1624
  };
1217
1625
  }
@@ -1407,7 +1815,6 @@ var StorageOAuthClientProvider = class {
1407
1815
  __publicField(this, "_authUrl");
1408
1816
  __publicField(this, "_clientId");
1409
1817
  __publicField(this, "onRedirectCallback");
1410
- __publicField(this, "tokenExpiresAt");
1411
1818
  this.userId = options.userId;
1412
1819
  this.serverId = options.serverId;
1413
1820
  this.sessionId = options.sessionId;
@@ -1441,30 +1848,30 @@ var StorageOAuthClientProvider = class {
1441
1848
  this._clientId = clientId_;
1442
1849
  }
1443
1850
  /**
1444
- * Loads OAuth data from the session store
1851
+ * Loads OAuth credentials from the session store
1445
1852
  * @private
1446
1853
  */
1447
- async getSessionData() {
1448
- const data = await sessions.get(this.userId, this.sessionId);
1854
+ async getCredentials() {
1855
+ const data = await sessions.getCredentials(this.userId, this.sessionId);
1449
1856
  if (!data) {
1450
- return {};
1857
+ return { userId: this.userId, sessionId: this.sessionId };
1451
1858
  }
1452
1859
  return data;
1453
1860
  }
1454
1861
  /**
1455
- * Saves OAuth data to the session store
1456
- * @param data - Partial OAuth data to save
1862
+ * Saves OAuth credentials to the session store
1863
+ * @param data - Partial OAuth credentials to save
1457
1864
  * @private
1458
1865
  * @throws Error if session doesn't exist (session must be created by controller layer)
1459
1866
  */
1460
- async saveSessionData(data) {
1461
- await sessions.update(this.userId, this.sessionId, data);
1867
+ async patchCredentials(data) {
1868
+ await sessions.patchCredentials(this.userId, this.sessionId, data);
1462
1869
  }
1463
1870
  /**
1464
1871
  * Retrieves stored OAuth client information
1465
1872
  */
1466
1873
  async clientInformation() {
1467
- const data = await this.getSessionData();
1874
+ const data = await this.getCredentials();
1468
1875
  if (data.clientId && !this._clientId) {
1469
1876
  this._clientId = data.clientId;
1470
1877
  }
@@ -1483,7 +1890,7 @@ var StorageOAuthClientProvider = class {
1483
1890
  * Stores OAuth client information
1484
1891
  */
1485
1892
  async saveClientInformation(clientInformation) {
1486
- await this.saveSessionData({
1893
+ await this.patchCredentials({
1487
1894
  clientInformation,
1488
1895
  clientId: clientInformation.client_id
1489
1896
  });
@@ -1493,29 +1900,58 @@ var StorageOAuthClientProvider = class {
1493
1900
  * Stores OAuth tokens
1494
1901
  */
1495
1902
  async saveTokens(tokens) {
1496
- const data = { tokens };
1497
- if (tokens.expires_in) {
1498
- this.tokenExpiresAt = Date.now() + tokens.expires_in * 1e3 - TOKEN_EXPIRY_BUFFER_MS;
1499
- }
1500
- await this.saveSessionData(data);
1903
+ await this.patchCredentials({ tokens });
1501
1904
  }
1502
1905
  get authUrl() {
1503
1906
  return this._authUrl;
1504
1907
  }
1505
1908
  async state() {
1506
- return this.sessionId;
1909
+ const nonce = nanoid(32);
1910
+ await this.patchCredentials({
1911
+ oauthState: {
1912
+ nonce,
1913
+ sessionId: this.sessionId,
1914
+ serverId: this.serverId,
1915
+ createdAt: Date.now()
1916
+ },
1917
+ codeVerifier: null
1918
+ });
1919
+ return formatOAuthState(nonce, this.sessionId);
1507
1920
  }
1508
- async checkState(_state) {
1509
- const data = await sessions.get(this.userId, this.sessionId);
1921
+ async checkState(state) {
1922
+ const parsed = parseOAuthState(state);
1923
+ if (!parsed) {
1924
+ return { valid: false, error: "Invalid OAuth state" };
1925
+ }
1926
+ if (parsed.sessionId !== this.sessionId) {
1927
+ return { valid: false, error: "OAuth state mismatch" };
1928
+ }
1929
+ const data = await sessions.getCredentials(this.userId, parsed.sessionId);
1510
1930
  if (!data) {
1511
1931
  return { valid: false, error: "Session not found" };
1512
1932
  }
1513
- return { valid: true, serverId: this.serverId };
1933
+ const oauthState = data.oauthState;
1934
+ if (!oauthState) {
1935
+ return { valid: false, error: "OAuth state not found" };
1936
+ }
1937
+ if (oauthState.nonce !== parsed.nonce || oauthState.sessionId !== parsed.sessionId || oauthState.serverId !== this.serverId) {
1938
+ return { valid: false, error: "OAuth state mismatch" };
1939
+ }
1940
+ if (Date.now() - oauthState.createdAt > STATE_EXPIRATION_MS) {
1941
+ return { valid: false, error: "OAuth state expired" };
1942
+ }
1943
+ return { valid: true, serverId: oauthState.serverId };
1514
1944
  }
1515
- async consumeState(_state) {
1945
+ async consumeState(state) {
1946
+ const result = await this.checkState(state);
1947
+ if (!result.valid) {
1948
+ throw new Error(result.error || "Invalid OAuth state");
1949
+ }
1950
+ await this.patchCredentials({ oauthState: null });
1516
1951
  }
1517
1952
  async redirectToAuthorization(authUrl) {
1518
1953
  this._authUrl = authUrl.toString();
1954
+ await sessions.update(this.userId, this.sessionId, { authUrl: this._authUrl });
1519
1955
  if (this.onRedirectCallback) {
1520
1956
  this.onRedirectCallback(authUrl.toString());
1521
1957
  }
@@ -1526,21 +1962,25 @@ var StorageOAuthClientProvider = class {
1526
1962
  } else {
1527
1963
  const updates = {};
1528
1964
  if (scope === "client") {
1529
- updates.clientInformation = void 0;
1530
- updates.clientId = void 0;
1965
+ updates.clientInformation = null;
1966
+ updates.clientId = null;
1531
1967
  } else if (scope === "tokens") {
1532
- updates.tokens = void 0;
1968
+ updates.tokens = null;
1533
1969
  } else if (scope === "verifier") {
1534
- updates.codeVerifier = void 0;
1970
+ updates.codeVerifier = null;
1535
1971
  }
1536
- await this.saveSessionData(updates);
1972
+ await this.patchCredentials(updates);
1537
1973
  }
1538
1974
  }
1539
1975
  async saveCodeVerifier(verifier) {
1540
- await this.saveSessionData({ codeVerifier: verifier });
1976
+ const data = await this.getCredentials();
1977
+ if (data.codeVerifier) {
1978
+ return;
1979
+ }
1980
+ await this.patchCredentials({ codeVerifier: verifier });
1541
1981
  }
1542
1982
  async codeVerifier() {
1543
- const data = await this.getSessionData();
1983
+ const data = await this.getCredentials();
1544
1984
  if (data.clientId && !this._clientId) {
1545
1985
  this._clientId = data.clientId;
1546
1986
  }
@@ -1550,23 +1990,14 @@ var StorageOAuthClientProvider = class {
1550
1990
  return data.codeVerifier;
1551
1991
  }
1552
1992
  async deleteCodeVerifier() {
1553
- await this.saveSessionData({ codeVerifier: void 0 });
1993
+ await this.patchCredentials({ codeVerifier: null });
1554
1994
  }
1555
1995
  async tokens() {
1556
- const data = await this.getSessionData();
1996
+ const data = await this.getCredentials();
1557
1997
  if (data.clientId && !this._clientId) {
1558
1998
  this._clientId = data.clientId;
1559
1999
  }
1560
- return data.tokens;
1561
- }
1562
- isTokenExpired() {
1563
- if (!this.tokenExpiresAt) {
1564
- return false;
1565
- }
1566
- return Date.now() >= this.tokenExpiresAt;
1567
- }
1568
- setTokenExpiresAt(expiresAt) {
1569
- this.tokenExpiresAt = expiresAt;
2000
+ return data.tokens ?? void 0;
1570
2001
  }
1571
2002
  };
1572
2003
 
@@ -1888,17 +2319,18 @@ var MCPClient = class {
1888
2319
  }
1889
2320
  this.emitStateChange("INITIALIZING");
1890
2321
  this.emitProgress("Loading session configuration...");
2322
+ let existingSession = null;
1891
2323
  if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
1892
- const sessionData = await sessions.get(this.userId, this.sessionId);
1893
- if (!sessionData) {
2324
+ existingSession = await sessions.get(this.userId, this.sessionId);
2325
+ if (!existingSession) {
1894
2326
  throw new Error(`Session not found: ${this.sessionId}`);
1895
2327
  }
1896
- this.serverUrl = this.serverUrl || sessionData.serverUrl;
1897
- this.callbackUrl = this.callbackUrl || sessionData.callbackUrl;
1898
- this.serverName = this.serverName || sessionData.serverName;
1899
- this.serverId = this.serverId || sessionData.serverId || "unknown";
1900
- this.headers = this.headers || sessionData.headers;
1901
- this.createdAt = sessionData.createdAt;
2328
+ this.serverUrl = this.serverUrl || existingSession.serverUrl;
2329
+ this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
2330
+ this.serverName = this.serverName || existingSession.serverName;
2331
+ this.serverId = this.serverId || existingSession.serverId || "unknown";
2332
+ this.headers = this.headers || existingSession.headers;
2333
+ this.createdAt = existingSession.createdAt;
1902
2334
  }
1903
2335
  if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
1904
2336
  throw new Error("Missing required connection metadata");
@@ -1942,10 +2374,13 @@ var MCPClient = class {
1942
2374
  }
1943
2375
  );
1944
2376
  }
1945
- const existingSession = await sessions.get(this.userId, this.sessionId);
2377
+ if (existingSession === null) {
2378
+ existingSession = await sessions.get(this.userId, this.sessionId);
2379
+ }
1946
2380
  if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
1947
2381
  this.createdAt = Date.now();
1948
- console.log(`[MCPClient] Creating initial session ${this.sessionId} for OAuth flow`);
2382
+ const updatedAt = this.createdAt;
2383
+ console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
1949
2384
  await sessions.create({
1950
2385
  sessionId: this.sessionId,
1951
2386
  userId: this.userId,
@@ -1956,18 +2391,18 @@ var MCPClient = class {
1956
2391
  transportType: this.transportType || "streamable-http",
1957
2392
  headers: this.headers,
1958
2393
  createdAt: this.createdAt,
1959
- active: false
1960
- }, Math.floor(STATE_EXPIRATION_MS / 1e3));
2394
+ updatedAt,
2395
+ status: "pending"
2396
+ });
1961
2397
  }
1962
2398
  }
1963
2399
  /**
1964
2400
  * Saves current session state to the session store
1965
2401
  * Creates new session if it doesn't exist, updates if it does
1966
- * @param ttl - Time-to-live in seconds (defaults to 12hr for connected sessions)
1967
- * @param active - Session status marker used to avoid unnecessary TTL rewrites
2402
+ * @param status - Session lifecycle status used by storage cleanup
1968
2403
  * @private
1969
2404
  */
1970
- async saveSession(ttl = SESSION_TTL_SECONDS, active = true) {
2405
+ async saveSession(status = "active", existingSession) {
1971
2406
  if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
1972
2407
  return;
1973
2408
  }
@@ -1981,13 +2416,29 @@ var MCPClient = class {
1981
2416
  transportType: this.transportType || "streamable-http",
1982
2417
  headers: this.headers,
1983
2418
  createdAt: this.createdAt || Date.now(),
1984
- active
2419
+ updatedAt: Date.now(),
2420
+ status
1985
2421
  };
1986
- const existingSession = await sessions.get(this.userId, this.sessionId);
2422
+ if (status === "active") {
2423
+ sessionData.authUrl = null;
2424
+ }
2425
+ if (existingSession === void 0) {
2426
+ existingSession = await sessions.get(this.userId, this.sessionId);
2427
+ }
1987
2428
  if (existingSession) {
1988
- await sessions.update(this.userId, this.sessionId, sessionData, ttl);
2429
+ await sessions.update(this.userId, this.sessionId, sessionData);
1989
2430
  } else {
1990
- await sessions.create(sessionData, ttl);
2431
+ await sessions.create(sessionData);
2432
+ }
2433
+ }
2434
+ /**
2435
+ * Removes transient setup/auth sessions without masking the original error.
2436
+ * @private
2437
+ */
2438
+ async deleteTransientSession() {
2439
+ try {
2440
+ await sessions.delete(this.userId, this.sessionId);
2441
+ } catch {
1991
2442
  }
1992
2443
  }
1993
2444
  /**
@@ -2052,8 +2503,8 @@ var MCPClient = class {
2052
2503
  this.transportType = transportType;
2053
2504
  this.emitStateChange("CONNECTED");
2054
2505
  this.emitProgress("Connected successfully");
2055
- console.log(`[MCPClient] Saving session ${this.sessionId} with 12hr TTL (connect success)`);
2056
- await this.saveSession(SESSION_TTL_SECONDS, true);
2506
+ console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
2507
+ await this.saveSession("active");
2057
2508
  } catch (error) {
2058
2509
  if (error instanceof UnauthorizedError$1 || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
2059
2510
  let authUrl = "";
@@ -2065,15 +2516,12 @@ var MCPClient = class {
2065
2516
  const message = detail.toLowerCase() === "unauthorized" ? "OAuth authorization URL not available" : `OAuth authorization URL not available: ${detail}`;
2066
2517
  this.emitError(message, "auth");
2067
2518
  this.emitStateChange("FAILED");
2068
- try {
2069
- await sessions.delete(this.userId, this.sessionId);
2070
- } catch {
2071
- }
2519
+ await this.deleteTransientSession();
2072
2520
  throw new Error(message);
2073
2521
  }
2074
2522
  this.emitStateChange("AUTHENTICATING");
2075
- console.log(`[MCPClient] Saving session ${this.sessionId} with 10min TTL (OAuth pending)`);
2076
- await this.saveSession(Math.floor(STATE_EXPIRATION_MS / 1e3), false);
2523
+ console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
2524
+ await this.saveSession("pending");
2077
2525
  if (this.serverId) {
2078
2526
  this._onConnectionEvent.fire({
2079
2527
  type: "auth_required",
@@ -2093,7 +2541,7 @@ var MCPClient = class {
2093
2541
  this.emitStateChange("FAILED");
2094
2542
  try {
2095
2543
  const existingSession = await sessions.get(this.userId, this.sessionId);
2096
- if (!existingSession || existingSession.active !== true) {
2544
+ if (!existingSession || existingSession.status !== "active") {
2097
2545
  await sessions.delete(this.userId, this.sessionId);
2098
2546
  }
2099
2547
  } catch {
@@ -2108,7 +2556,7 @@ var MCPClient = class {
2108
2556
  * @param authCode - Authorization code received from OAuth callback
2109
2557
  */
2110
2558
  // TODO: needs to be optimized
2111
- async finishAuth(authCode) {
2559
+ async finishAuth(authCode, state) {
2112
2560
  this.emitStateChange("AUTHENTICATING");
2113
2561
  this.emitProgress("Exchanging authorization code for tokens...");
2114
2562
  await this.initialize();
@@ -2118,6 +2566,16 @@ var MCPClient = class {
2118
2566
  this.emitStateChange("FAILED");
2119
2567
  throw new Error(error);
2120
2568
  }
2569
+ if (state) {
2570
+ const stateCheck = await this.oauthProvider.checkState(state);
2571
+ if (!stateCheck.valid) {
2572
+ const error = stateCheck.error || "Invalid OAuth state";
2573
+ this.emitError(error, "auth");
2574
+ this.emitStateChange("FAILED");
2575
+ throw new Error(error);
2576
+ }
2577
+ await this.oauthProvider.consumeState(state);
2578
+ }
2121
2579
  const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
2122
2580
  let lastError;
2123
2581
  let tokensExchanged = false;
@@ -2157,8 +2615,8 @@ var MCPClient = class {
2157
2615
  await this.client.connect(this.transport);
2158
2616
  this.transportType = currentType;
2159
2617
  this.emitStateChange("CONNECTED");
2160
- console.log(`[MCPClient] Updating session ${this.sessionId} to 12hr TTL (OAuth complete)`);
2161
- await this.saveSession(SESSION_TTL_SECONDS, true);
2618
+ console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
2619
+ await this.saveSession("active");
2162
2620
  return;
2163
2621
  } catch (error) {
2164
2622
  lastError = error;
@@ -2171,12 +2629,14 @@ var MCPClient = class {
2171
2629
  const msg = error instanceof Error ? error.message : "Authentication failed";
2172
2630
  this.emitError(msg, "auth");
2173
2631
  this.emitStateChange("FAILED");
2632
+ await this.deleteTransientSession();
2174
2633
  throw error;
2175
2634
  }
2176
2635
  if (isLastAttempt) {
2177
2636
  const msg = error instanceof Error ? error.message : "Authentication failed";
2178
2637
  this.emitError(msg, "auth");
2179
2638
  this.emitStateChange("FAILED");
2639
+ await this.deleteTransientSession();
2180
2640
  throw error;
2181
2641
  }
2182
2642
  this.emitProgress(`Auth attempt with ${currentType} failed: ${errorMessage}. Retrying...`);
@@ -2186,6 +2646,7 @@ var MCPClient = class {
2186
2646
  const errorMessage = lastError instanceof Error ? lastError.message : "Authentication failed";
2187
2647
  this.emitError(errorMessage, "auth");
2188
2648
  this.emitStateChange("FAILED");
2649
+ await this.deleteTransientSession();
2189
2650
  throw lastError;
2190
2651
  }
2191
2652
  }
@@ -2500,48 +2961,6 @@ var MCPClient = class {
2500
2961
  getSessionId() {
2501
2962
  return this.sessionId;
2502
2963
  }
2503
- /**
2504
- * Gets MCP server configuration for all active user sessions
2505
- * Loads sessions from storage and returns server connection metadata.
2506
- * OAuth refresh is handled by SDK transports through their authProvider.
2507
- * @deprecated This returns legacy connection metadata only and does not
2508
- * include OAuth tokens or generated Authorization headers. Prefer
2509
- * MultiSessionClient or explicit MCPClient instances so SDK transports can
2510
- * own OAuth refresh and reauthorization.
2511
- * @param userId - User ID to fetch sessions for
2512
- * @returns Object keyed by sanitized server labels containing transport and url.
2513
- * @static
2514
- */
2515
- static async getMcpServerConfig(userId) {
2516
- const mcpConfig = {};
2517
- const sessionList = await sessions.list(userId);
2518
- await Promise.all(
2519
- sessionList.map(async (sessionData) => {
2520
- const { sessionId } = sessionData;
2521
- try {
2522
- if (!sessionData.serverId || !sessionData.transportType || !sessionData.serverUrl || !sessionData.callbackUrl) {
2523
- await sessions.delete(userId, sessionId);
2524
- return;
2525
- }
2526
- const label = sanitizeServerLabel(
2527
- sessionData.serverName || sessionData.serverId || "server"
2528
- );
2529
- mcpConfig[label] = {
2530
- transport: sessionData.transportType,
2531
- url: sessionData.serverUrl,
2532
- ...sessionData.serverName && {
2533
- serverName: sessionData.serverName,
2534
- serverLabel: label
2535
- }
2536
- };
2537
- } catch (error) {
2538
- await sessions.delete(userId, sessionId);
2539
- console.warn(`[MCP] Failed to process session ${sessionId}:`, error);
2540
- }
2541
- })
2542
- );
2543
- return mcpConfig;
2544
- }
2545
2964
  };
2546
2965
 
2547
2966
  // src/server/mcp/multi-session-client.ts
@@ -2576,18 +2995,13 @@ var MultiSessionClient = class {
2576
2995
  *
2577
2996
  * A session is considered connectable when:
2578
2997
  * - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
2579
- * - Its `active` flag is not explicitly `false` sessions with `active: false` are
2580
- * either mid-OAuth flow, auth-pending, or previously failed. We skip those here
2998
+ * - Its status is `active`. Pending sessions are skipped here
2581
2999
  * and let the OAuth flow complete separately before we try to reconnect them.
2582
- *
2583
- * Note: Sessions where `active` is `undefined` (legacy records) are included
2584
- * for backwards compatibility.
2585
3000
  */
2586
3001
  async getActiveSessions() {
2587
3002
  const sessionList = await sessions.list(this.userId);
2588
3003
  const valid = sessionList.filter(
2589
- (s) => s.serverId && s.serverUrl && s.callbackUrl && s.active !== false
2590
- // exclude OAuth-pending / failed sessions
3004
+ (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
2591
3005
  );
2592
3006
  return valid;
2593
3007
  }
@@ -2858,7 +3272,8 @@ var SSEConnectionManager = class {
2858
3272
  serverUrl: s.serverUrl,
2859
3273
  transport: s.transportType,
2860
3274
  createdAt: s.createdAt,
2861
- active: s.active !== false
3275
+ updatedAt: s.updatedAt ?? s.createdAt,
3276
+ status: s.status ?? "pending"
2862
3277
  }))
2863
3278
  };
2864
3279
  }
@@ -2874,7 +3289,7 @@ var SSEConnectionManager = class {
2874
3289
  (s) => s.serverId === serverId || s.serverUrl === serverUrl
2875
3290
  );
2876
3291
  if (duplicate) {
2877
- if (duplicate.active === false) {
3292
+ if (duplicate.status === "pending") {
2878
3293
  await this.getSession({ sessionId: duplicate.sessionId });
2879
3294
  return {
2880
3295
  sessionId: duplicate.sessionId,
@@ -3061,7 +3476,10 @@ var SSEConnectionManager = class {
3061
3476
  * Complete OAuth authorization flow
3062
3477
  */
3063
3478
  async finishAuth(params) {
3064
- const { sessionId, code } = params;
3479
+ const { code } = params;
3480
+ const oauthState = params.state;
3481
+ const parsedState = parseOAuthState(oauthState);
3482
+ const sessionId = parsedState?.sessionId || oauthState;
3065
3483
  const session = await sessions.get(this.userId, sessionId);
3066
3484
  if (!session) {
3067
3485
  throw new Error("Session not found");
@@ -3086,7 +3504,7 @@ var SSEConnectionManager = class {
3086
3504
  headers: session.headers
3087
3505
  });
3088
3506
  client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3089
- await client.finishAuth(code);
3507
+ await client.finishAuth(code, oauthState);
3090
3508
  this.clients.set(sessionId, client);
3091
3509
  const tools = await client.listTools();
3092
3510
  return { success: true, toolCount: tools.tools.length };
@@ -3391,8 +3809,8 @@ var SSEClient = class {
3391
3809
  async getSession(sessionId) {
3392
3810
  return this.sendRequest("getSession", { sessionId });
3393
3811
  }
3394
- async finishAuth(sessionId, code) {
3395
- return this.sendRequest("finishAuth", { sessionId, code });
3812
+ async finishAuth(state, code) {
3813
+ return this.sendRequest("finishAuth", { state, code });
3396
3814
  }
3397
3815
  async listPrompts(sessionId) {
3398
3816
  return this.sendRequest("listPrompts", { sessionId });
@@ -5246,6 +5664,6 @@ function globToRegExp(pattern) {
5246
5664
  return new RegExp(regexPattern);
5247
5665
  }
5248
5666
 
5249
- export { APP_HOST_DEFAULTS, AppHost, AuthenticationError, ConfigurationError, ConnectionError, DEFAULT_CLIENT_NAME, DEFAULT_CLIENT_URI, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_LOGO_URI, DEFAULT_MCP_APP_CSP, DEFAULT_POLICY_URI, DisposableStore, Emitter, InvalidStateError, MCPClient, MCP_CLIENT_NAME, MCP_CLIENT_VERSION, McpError, MultiSessionClient, NotConnectedError, REDIS_KEY_PREFIX, RpcErrorCodes, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, SESSION_TTL_SECONDS, SOFTWARE_ID, SOFTWARE_VERSION, SSEClient, SSEConnectionManager, STATE_EXPIRATION_MS, SchemaCompressor, SessionNotFoundError, SessionValidationError, StorageOAuthClientProvider, TOKEN_EXPIRY_BUFFER_MS, ToolExecutionError, ToolIndex, ToolRouter, UnauthorizedError, createExecuteToolDefinition, createGetSchemaToolDefinition, createListServersToolDefinition, createNextMcpHandler, createRegexSearchToolDefinition, createSSEHandler, createSearchToolDefinition, executeMetaTool, findToolByName, getToolUiResourceUri, isCallToolSuccess, isConnectAuthRequired, isConnectError, isConnectSuccess, isListToolsSuccess, isMetaTool, onSessionMutation, resolveMetaToolProxy, sanitizeServerLabel, sessions };
5667
+ export { APP_HOST_DEFAULTS, AppHost, AuthenticationError, ConfigurationError, ConnectionError, DEFAULT_CLIENT_NAME, DEFAULT_CLIENT_URI, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_LOGO_URI, DEFAULT_MCP_APP_CSP, DEFAULT_POLICY_URI, DORMANT_SESSION_EXPIRATION_MS, DORMANT_SESSION_EXPIRATION_SECONDS, DisposableStore, Emitter, InvalidStateError, MCPClient, MCP_CLIENT_NAME, MCP_CLIENT_VERSION, McpError, MultiSessionClient, NotConnectedError, PENDING_SESSION_EXPIRATION_SECONDS, REDIS_KEY_PREFIX, RpcErrorCodes, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, SOFTWARE_ID, SOFTWARE_VERSION, SSEClient, SSEConnectionManager, STATE_EXPIRATION_MS, SchemaCompressor, SessionNotFoundError, SessionValidationError, StorageOAuthClientProvider, ToolExecutionError, ToolIndex, ToolRouter, UnauthorizedError, createExecuteToolDefinition, createGetSchemaToolDefinition, createListServersToolDefinition, createNextMcpHandler, createRegexSearchToolDefinition, createSSEHandler, createSearchToolDefinition, executeMetaTool, findToolByName, getToolUiResourceUri, isCallToolSuccess, isConnectAuthRequired, isConnectError, isConnectSuccess, isListToolsSuccess, isMetaTool, onSessionMutation, resolveMetaToolProxy, sanitizeServerLabel, sessions };
5250
5668
  //# sourceMappingURL=index.mjs.map
5251
5669
  //# sourceMappingURL=index.mjs.map