@mcp-ts/sdk 2.3.4 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +161 -39
  2. package/dist/adapters/agui-adapter.d.mts +4 -4
  3. package/dist/adapters/agui-adapter.d.ts +4 -4
  4. package/dist/adapters/agui-middleware.d.mts +4 -4
  5. package/dist/adapters/agui-middleware.d.ts +4 -4
  6. package/dist/adapters/ai-adapter.d.mts +4 -4
  7. package/dist/adapters/ai-adapter.d.ts +4 -4
  8. package/dist/adapters/langchain-adapter.d.mts +4 -4
  9. package/dist/adapters/langchain-adapter.d.ts +4 -4
  10. package/dist/adapters/mastra-adapter.d.mts +2 -2
  11. package/dist/adapters/mastra-adapter.d.ts +2 -2
  12. package/dist/client/index.d.mts +3 -3
  13. package/dist/client/index.d.ts +3 -3
  14. package/dist/client/index.js +2 -2
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/index.mjs +2 -2
  17. package/dist/client/index.mjs.map +1 -1
  18. package/dist/client/react.d.mts +11 -10
  19. package/dist/client/react.d.ts +11 -10
  20. package/dist/client/react.js +66 -26
  21. package/dist/client/react.js.map +1 -1
  22. package/dist/client/react.mjs +67 -27
  23. package/dist/client/react.mjs.map +1 -1
  24. package/dist/client/vue.d.mts +8 -7
  25. package/dist/client/vue.d.ts +8 -7
  26. package/dist/client/vue.js +27 -17
  27. package/dist/client/vue.js.map +1 -1
  28. package/dist/client/vue.mjs +27 -17
  29. package/dist/client/vue.mjs.map +1 -1
  30. package/dist/{events-CK3N--3g.d.mts → events-C4m7tK1U.d.mts} +0 -1
  31. package/dist/{events-CK3N--3g.d.ts → events-C4m7tK1U.d.ts} +0 -1
  32. package/dist/{index-CmjMd2ac.d.ts → index-Ch7ouNSa.d.ts} +3 -3
  33. package/dist/{index-Cfjsme-a.d.mts → index-L5XoXgsb.d.mts} +3 -3
  34. package/dist/index.d.mts +6 -6
  35. package/dist/index.d.ts +6 -6
  36. package/dist/index.js +799 -326
  37. package/dist/index.js.map +1 -1
  38. package/dist/index.mjs +793 -324
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/{multi-session-client-C_kPHpD5.d.ts → multi-session-client-Bwm-efqg.d.ts} +43 -38
  41. package/dist/{multi-session-client-C7hGqzgM.d.mts → multi-session-client-k-9RvWvi.d.mts} +43 -38
  42. package/dist/server/index.d.mts +48 -20
  43. package/dist/server/index.d.ts +48 -20
  44. package/dist/server/index.js +791 -320
  45. package/dist/server/index.js.map +1 -1
  46. package/dist/server/index.mjs +787 -319
  47. package/dist/server/index.mjs.map +1 -1
  48. package/dist/shared/index.d.mts +10 -9
  49. package/dist/shared/index.d.ts +10 -9
  50. package/dist/shared/index.js +7 -5
  51. package/dist/shared/index.js.map +1 -1
  52. package/dist/shared/index.mjs +5 -4
  53. package/dist/shared/index.mjs.map +1 -1
  54. package/dist/{tool-router-BMzhoNYt.d.ts → tool-router-CZMrOG8J.d.ts} +1 -1
  55. package/dist/{tool-router-BhHsvBfP.d.mts → tool-router-CuApsDiV.d.mts} +1 -1
  56. package/dist/{types-CxFaaZrM.d.mts → types-DCk_IF4L.d.mts} +5 -3
  57. package/dist/{types-CxFaaZrM.d.ts → types-DCk_IF4L.d.ts} +5 -3
  58. package/migrations/neon/20260513010000_install_mcp_sessions.sql +40 -3
  59. package/migrations/neon/20260513020000_add_session_cleanup_cron.sql +4 -4
  60. package/migrations/supabase/20260330195700_install_mcp_sessions.sql +67 -3
  61. package/migrations/supabase/20260421010000_add_session_cleanup_cron.sql +6 -6
  62. package/migrations/v2.3.4/neon/20260513010000_install_mcp_sessions.sql +69 -0
  63. package/migrations/v2.3.4/neon/20260513020000_add_session_cleanup_cron.sql +35 -0
  64. package/migrations/v2.3.4/supabase/20260330195700_install_mcp_sessions.sql +82 -0
  65. package/migrations/v2.3.4/supabase/20260421010000_add_session_cleanup_cron.sql +32 -0
  66. package/package.json +13 -3
  67. package/src/client/core/sse-client.ts +2 -2
  68. package/src/client/react/index.ts +1 -1
  69. package/src/client/react/oauth-popup.tsx +34 -13
  70. package/src/client/react/use-mcp.ts +19 -45
  71. package/src/client/utils/session-state.ts +43 -0
  72. package/src/client/vue/use-mcp.ts +18 -47
  73. package/src/server/handlers/sse-handler.ts +16 -9
  74. package/src/server/mcp/multi-session-client.ts +48 -11
  75. package/src/server/mcp/oauth-client.ts +133 -111
  76. package/src/server/mcp/storage-oauth-provider.ts +79 -50
  77. package/src/server/storage/file-backend.ts +74 -18
  78. package/src/server/storage/index.ts +2 -4
  79. package/src/server/storage/memory-backend.ts +49 -13
  80. package/src/server/storage/neon-backend.ts +201 -68
  81. package/src/server/storage/redis-backend.ts +81 -23
  82. package/src/server/storage/session-lifecycle.ts +78 -0
  83. package/src/server/storage/sqlite-backend.ts +89 -15
  84. package/src/server/storage/supabase-backend.ts +188 -63
  85. package/src/server/storage/types.ts +49 -16
  86. package/src/shared/constants.ts +5 -6
  87. package/src/shared/events.ts +0 -1
  88. package/src/shared/types.ts +5 -3
  89. package/src/shared/utils.ts +26 -0
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
 
@@ -1859,7 +2290,7 @@ var MCPClient = class {
1859
2290
  * Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
1860
2291
  * This wrapper enforces a timeout and properly uses AbortController to unblock the request.
1861
2292
  */
1862
- fetch: (url, init) => {
2293
+ fetch: async (url, init) => {
1863
2294
  const timeout = 3e4;
1864
2295
  const controller = new AbortController();
1865
2296
  const timeoutId = setTimeout(() => controller.abort(), timeout);
@@ -1867,7 +2298,17 @@ var MCPClient = class {
1867
2298
  // @ts-ignore: AbortSignal.any is available in Node 20+
1868
2299
  AbortSignal.any ? AbortSignal.any([init.signal, controller.signal]) : controller.signal
1869
2300
  ) : controller.signal;
1870
- return fetch(url, { ...init, signal }).finally(() => clearTimeout(timeoutId));
2301
+ try {
2302
+ const response = await fetch(url, { ...init, signal });
2303
+ const hasSessionHeader = init?.headers && new Headers(init.headers).has("mcp-session-id");
2304
+ if (response.status === 404 && hasSessionHeader) {
2305
+ this.client = null;
2306
+ throw new Error("MCP_SESSION_EXPIRED: Downstream session was not found on the server.");
2307
+ }
2308
+ return response;
2309
+ } finally {
2310
+ clearTimeout(timeoutId);
2311
+ }
1871
2312
  }
1872
2313
  };
1873
2314
  if (type === "sse") {
@@ -1888,17 +2329,18 @@ var MCPClient = class {
1888
2329
  }
1889
2330
  this.emitStateChange("INITIALIZING");
1890
2331
  this.emitProgress("Loading session configuration...");
2332
+ let existingSession = null;
1891
2333
  if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
1892
- const sessionData = await sessions.get(this.userId, this.sessionId);
1893
- if (!sessionData) {
2334
+ existingSession = await sessions.get(this.userId, this.sessionId);
2335
+ if (!existingSession) {
1894
2336
  throw new Error(`Session not found: ${this.sessionId}`);
1895
2337
  }
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;
2338
+ this.serverUrl = this.serverUrl || existingSession.serverUrl;
2339
+ this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
2340
+ this.serverName = this.serverName || existingSession.serverName;
2341
+ this.serverId = this.serverId || existingSession.serverId || "unknown";
2342
+ this.headers = this.headers || existingSession.headers;
2343
+ this.createdAt = existingSession.createdAt;
1902
2344
  }
1903
2345
  if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
1904
2346
  throw new Error("Missing required connection metadata");
@@ -1942,10 +2384,13 @@ var MCPClient = class {
1942
2384
  }
1943
2385
  );
1944
2386
  }
1945
- const existingSession = await sessions.get(this.userId, this.sessionId);
2387
+ if (existingSession === null) {
2388
+ existingSession = await sessions.get(this.userId, this.sessionId);
2389
+ }
1946
2390
  if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
1947
2391
  this.createdAt = Date.now();
1948
- console.log(`[MCPClient] Creating initial session ${this.sessionId} for OAuth flow`);
2392
+ const updatedAt = this.createdAt;
2393
+ console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
1949
2394
  await sessions.create({
1950
2395
  sessionId: this.sessionId,
1951
2396
  userId: this.userId,
@@ -1956,18 +2401,18 @@ var MCPClient = class {
1956
2401
  transportType: this.transportType || "streamable-http",
1957
2402
  headers: this.headers,
1958
2403
  createdAt: this.createdAt,
1959
- active: false
1960
- }, Math.floor(STATE_EXPIRATION_MS / 1e3));
2404
+ updatedAt,
2405
+ status: "pending"
2406
+ });
1961
2407
  }
1962
2408
  }
1963
2409
  /**
1964
2410
  * Saves current session state to the session store
1965
2411
  * 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
2412
+ * @param status - Session lifecycle status used by storage cleanup
1968
2413
  * @private
1969
2414
  */
1970
- async saveSession(ttl = SESSION_TTL_SECONDS, active = true) {
2415
+ async saveSession(status = "active", existingSession) {
1971
2416
  if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
1972
2417
  return;
1973
2418
  }
@@ -1981,13 +2426,29 @@ var MCPClient = class {
1981
2426
  transportType: this.transportType || "streamable-http",
1982
2427
  headers: this.headers,
1983
2428
  createdAt: this.createdAt || Date.now(),
1984
- active
2429
+ updatedAt: Date.now(),
2430
+ status
1985
2431
  };
1986
- const existingSession = await sessions.get(this.userId, this.sessionId);
2432
+ if (status === "active") {
2433
+ sessionData.authUrl = null;
2434
+ }
2435
+ if (existingSession === void 0) {
2436
+ existingSession = await sessions.get(this.userId, this.sessionId);
2437
+ }
1987
2438
  if (existingSession) {
1988
- await sessions.update(this.userId, this.sessionId, sessionData, ttl);
2439
+ await sessions.update(this.userId, this.sessionId, sessionData);
1989
2440
  } else {
1990
- await sessions.create(sessionData, ttl);
2441
+ await sessions.create(sessionData);
2442
+ }
2443
+ }
2444
+ /**
2445
+ * Removes transient setup/auth sessions without masking the original error.
2446
+ * @private
2447
+ */
2448
+ async deleteTransientSession() {
2449
+ try {
2450
+ await sessions.delete(this.userId, this.sessionId);
2451
+ } catch {
1991
2452
  }
1992
2453
  }
1993
2454
  /**
@@ -2039,6 +2500,14 @@ var MCPClient = class {
2039
2500
  * @throws {Error} When connection fails for other reasons
2040
2501
  */
2041
2502
  async connect() {
2503
+ if (this.client?.transport) {
2504
+ this.transport = null;
2505
+ try {
2506
+ await this.client.close();
2507
+ } catch {
2508
+ }
2509
+ this.client = null;
2510
+ }
2042
2511
  await this.initialize();
2043
2512
  if (!this.client || !this.oauthProvider) {
2044
2513
  const error = "Client or OAuth provider not initialized";
@@ -2052,8 +2521,8 @@ var MCPClient = class {
2052
2521
  this.transportType = transportType;
2053
2522
  this.emitStateChange("CONNECTED");
2054
2523
  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);
2524
+ console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
2525
+ await this.saveSession("active");
2057
2526
  } catch (error) {
2058
2527
  if (error instanceof UnauthorizedError$1 || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
2059
2528
  let authUrl = "";
@@ -2065,15 +2534,12 @@ var MCPClient = class {
2065
2534
  const message = detail.toLowerCase() === "unauthorized" ? "OAuth authorization URL not available" : `OAuth authorization URL not available: ${detail}`;
2066
2535
  this.emitError(message, "auth");
2067
2536
  this.emitStateChange("FAILED");
2068
- try {
2069
- await sessions.delete(this.userId, this.sessionId);
2070
- } catch {
2071
- }
2537
+ await this.deleteTransientSession();
2072
2538
  throw new Error(message);
2073
2539
  }
2074
2540
  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);
2541
+ console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
2542
+ await this.saveSession("pending");
2077
2543
  if (this.serverId) {
2078
2544
  this._onConnectionEvent.fire({
2079
2545
  type: "auth_required",
@@ -2093,7 +2559,7 @@ var MCPClient = class {
2093
2559
  this.emitStateChange("FAILED");
2094
2560
  try {
2095
2561
  const existingSession = await sessions.get(this.userId, this.sessionId);
2096
- if (!existingSession || existingSession.active !== true) {
2562
+ if (!existingSession || existingSession.status !== "active") {
2097
2563
  await sessions.delete(this.userId, this.sessionId);
2098
2564
  }
2099
2565
  } catch {
@@ -2108,7 +2574,7 @@ var MCPClient = class {
2108
2574
  * @param authCode - Authorization code received from OAuth callback
2109
2575
  */
2110
2576
  // TODO: needs to be optimized
2111
- async finishAuth(authCode) {
2577
+ async finishAuth(authCode, state) {
2112
2578
  this.emitStateChange("AUTHENTICATING");
2113
2579
  this.emitProgress("Exchanging authorization code for tokens...");
2114
2580
  await this.initialize();
@@ -2118,6 +2584,16 @@ var MCPClient = class {
2118
2584
  this.emitStateChange("FAILED");
2119
2585
  throw new Error(error);
2120
2586
  }
2587
+ if (state) {
2588
+ const stateCheck = await this.oauthProvider.checkState(state);
2589
+ if (!stateCheck.valid) {
2590
+ const error = stateCheck.error || "Invalid OAuth state";
2591
+ this.emitError(error, "auth");
2592
+ this.emitStateChange("FAILED");
2593
+ throw new Error(error);
2594
+ }
2595
+ await this.oauthProvider.consumeState(state);
2596
+ }
2121
2597
  const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
2122
2598
  let lastError;
2123
2599
  let tokensExchanged = false;
@@ -2157,8 +2633,8 @@ var MCPClient = class {
2157
2633
  await this.client.connect(this.transport);
2158
2634
  this.transportType = currentType;
2159
2635
  this.emitStateChange("CONNECTED");
2160
- console.log(`[MCPClient] Updating session ${this.sessionId} to 12hr TTL (OAuth complete)`);
2161
- await this.saveSession(SESSION_TTL_SECONDS, true);
2636
+ console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
2637
+ await this.saveSession("active");
2162
2638
  return;
2163
2639
  } catch (error) {
2164
2640
  lastError = error;
@@ -2171,12 +2647,14 @@ var MCPClient = class {
2171
2647
  const msg = error instanceof Error ? error.message : "Authentication failed";
2172
2648
  this.emitError(msg, "auth");
2173
2649
  this.emitStateChange("FAILED");
2650
+ await this.deleteTransientSession();
2174
2651
  throw error;
2175
2652
  }
2176
2653
  if (isLastAttempt) {
2177
2654
  const msg = error instanceof Error ? error.message : "Authentication failed";
2178
2655
  this.emitError(msg, "auth");
2179
2656
  this.emitStateChange("FAILED");
2657
+ await this.deleteTransientSession();
2180
2658
  throw error;
2181
2659
  }
2182
2660
  this.emitProgress(`Auth attempt with ${currentType} failed: ${errorMessage}. Retrying...`);
@@ -2186,6 +2664,7 @@ var MCPClient = class {
2186
2664
  const errorMessage = lastError instanceof Error ? lastError.message : "Authentication failed";
2187
2665
  this.emitError(errorMessage, "auth");
2188
2666
  this.emitStateChange("FAILED");
2667
+ await this.deleteTransientSession();
2189
2668
  throw lastError;
2190
2669
  }
2191
2670
  }
@@ -2407,7 +2886,7 @@ var MCPClient = class {
2407
2886
  await this.oauthProvider.invalidateCredentials("all");
2408
2887
  }
2409
2888
  await sessions.delete(this.userId, this.sessionId);
2410
- this.disconnect();
2889
+ await this.disconnect();
2411
2890
  }
2412
2891
  /**
2413
2892
  * Checks if the client is currently connected to an MCP server
@@ -2417,10 +2896,21 @@ var MCPClient = class {
2417
2896
  return this.client !== null;
2418
2897
  }
2419
2898
  /**
2420
- * Disconnects from the MCP server and cleans up resources
2421
- * Does not remove session from Redis - use clearSession() for that
2899
+ * Disconnects from the MCP server and cleans up resources.
2900
+ * Does not remove session from Redis use clearSession() for that.
2901
+ *
2902
+ * For Streamable HTTP sessions, sends an HTTP DELETE to the MCP endpoint
2903
+ * before closing, as recommended by the MCP Streamable HTTP spec
2904
+ * (section "Session Management", rule 5). This is best-effort — errors
2905
+ * (e.g. server already restarted, 404/405 responses) are silently ignored.
2422
2906
  */
2423
- disconnect(reason) {
2907
+ async disconnect() {
2908
+ if (this.transport instanceof StreamableHTTPClientTransport) {
2909
+ try {
2910
+ await this.transport.terminateSession();
2911
+ } catch {
2912
+ }
2913
+ }
2424
2914
  if (this.client) {
2425
2915
  this.client.close();
2426
2916
  }
@@ -2432,7 +2922,6 @@ var MCPClient = class {
2432
2922
  type: "disconnected",
2433
2923
  sessionId: this.sessionId,
2434
2924
  serverId: this.serverId,
2435
- reason,
2436
2925
  timestamp: Date.now()
2437
2926
  });
2438
2927
  this._onObservabilityEvent.fire({
@@ -2441,9 +2930,7 @@ var MCPClient = class {
2441
2930
  message: `Disconnected from ${this.serverId}`,
2442
2931
  sessionId: this.sessionId,
2443
2932
  serverId: this.serverId,
2444
- payload: {
2445
- reason: reason || "unknown"
2446
- },
2933
+ payload: {},
2447
2934
  timestamp: Date.now(),
2448
2935
  id: nanoid()
2449
2936
  });
@@ -2500,48 +2987,6 @@ var MCPClient = class {
2500
2987
  getSessionId() {
2501
2988
  return this.sessionId;
2502
2989
  }
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
2990
  };
2546
2991
 
2547
2992
  // src/server/mcp/multi-session-client.ts
@@ -2576,18 +3021,13 @@ var MultiSessionClient = class {
2576
3021
  *
2577
3022
  * A session is considered connectable when:
2578
3023
  * - 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
3024
+ * - Its status is `active`. Pending sessions are skipped here
2581
3025
  * 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
3026
  */
2586
3027
  async getActiveSessions() {
2587
3028
  const sessionList = await sessions.list(this.userId);
2588
3029
  const valid = sessionList.filter(
2589
- (s) => s.serverId && s.serverUrl && s.callbackUrl && s.active !== false
2590
- // exclude OAuth-pending / failed sessions
3030
+ (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
2591
3031
  );
2592
3032
  return valid;
2593
3033
  }
@@ -2608,6 +3048,9 @@ var MultiSessionClient = class {
2608
3048
  * Connects a single session, with built-in deduplication to prevent race conditions.
2609
3049
  *
2610
3050
  * - If a client for this session already exists and is connected, returns immediately.
3051
+ * - If the existing client entry is no longer connected (e.g. it was explicitly
3052
+ * disconnected), it is evicted so that `establishConnectionWithRetries` creates a
3053
+ * fresh transport — preventing "Client already connected" errors from the SDK.
2611
3054
  * - If a connection attempt for this session is already in-flight (e.g. from a
2612
3055
  * concurrent call), it joins the existing promise instead of starting a new one.
2613
3056
  * This is the key concurrency lock — the `connectionPromises` map acts as a
@@ -2615,9 +3058,12 @@ var MultiSessionClient = class {
2615
3058
  * - On completion (success or failure), the promise is cleaned up from the map.
2616
3059
  */
2617
3060
  async connectSession(session) {
2618
- const existingClient = this.clients.find((c) => c.getSessionId() === session.sessionId);
2619
- if (existingClient?.isConnected()) {
2620
- return;
3061
+ const existing = this.clients.find((c) => c.getSessionId() === session.sessionId);
3062
+ if (existing) {
3063
+ if (existing.isConnected()) {
3064
+ return;
3065
+ }
3066
+ this.clients = this.clients.filter((c) => c !== existing);
2621
3067
  }
2622
3068
  if (this.connectionPromises.has(session.sessionId)) {
2623
3069
  return this.connectionPromises.get(session.sessionId);
@@ -2692,8 +3138,24 @@ var MultiSessionClient = class {
2692
3138
  */
2693
3139
  async connect() {
2694
3140
  const sessions2 = await this.getActiveSessions();
3141
+ const activeSessionIds = new Set(sessions2.map((s) => s.sessionId));
3142
+ this.clients = this.clients.filter((c) => activeSessionIds.has(c.getSessionId()));
2695
3143
  await this.connectInBatches(sessions2);
2696
3144
  }
3145
+ /**
3146
+ * Drops all cached `MCPClient` instances and reconnects fresh from storage.
3147
+ *
3148
+ * Call this when downstream MCP servers have expired their transport sessions
3149
+ * (e.g. after a remote server restart) and subsequent tool calls return
3150
+ * "Session not found. Reconnect without session header." errors.
3151
+ *
3152
+ * OAuth tokens are preserved in the storage backend — no re-authentication
3153
+ * is required. Only the in-memory transport sessions are cleared.
3154
+ */
3155
+ async reconnect() {
3156
+ await this.disconnect();
3157
+ await this.connect();
3158
+ }
2697
3159
  /**
2698
3160
  * Returns all currently connected `MCPClient` instances.
2699
3161
  *
@@ -2706,11 +3168,15 @@ var MultiSessionClient = class {
2706
3168
  /**
2707
3169
  * Gracefully disconnects all active MCP clients and clears the internal client list.
2708
3170
  *
3171
+ * For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
3172
+ * endpoint per the spec before closing locally. All disconnects run in
3173
+ * parallel so shutdown is not serialised across many sessions.
3174
+ *
2709
3175
  * Call this during server shutdown or when a user logs out to free up
2710
3176
  * underlying transport resources (SSE streams, HTTP connections, etc.).
2711
3177
  */
2712
- disconnect() {
2713
- this.clients.forEach((client) => client.disconnect());
3178
+ async disconnect() {
3179
+ await Promise.all(this.clients.map((client) => client.disconnect()));
2714
3180
  this.clients = [];
2715
3181
  }
2716
3182
  };
@@ -2858,7 +3324,8 @@ var SSEConnectionManager = class {
2858
3324
  serverUrl: s.serverUrl,
2859
3325
  transport: s.transportType,
2860
3326
  createdAt: s.createdAt,
2861
- active: s.active !== false
3327
+ updatedAt: s.updatedAt ?? s.createdAt,
3328
+ status: s.status ?? "pending"
2862
3329
  }))
2863
3330
  };
2864
3331
  }
@@ -2874,7 +3341,7 @@ var SSEConnectionManager = class {
2874
3341
  (s) => s.serverId === serverId || s.serverUrl === serverUrl
2875
3342
  );
2876
3343
  if (duplicate) {
2877
- if (duplicate.active === false) {
3344
+ if (duplicate.status === "pending") {
2878
3345
  await this.getSession({ sessionId: duplicate.sessionId });
2879
3346
  return {
2880
3347
  sessionId: duplicate.sessionId,
@@ -2939,7 +3406,6 @@ var SSEConnectionManager = class {
2939
3406
  const client = this.clients.get(sessionId);
2940
3407
  if (client) {
2941
3408
  await client.clearSession();
2942
- client.disconnect();
2943
3409
  this.clients.delete(sessionId);
2944
3410
  } else {
2945
3411
  await sessions.delete(this.userId, sessionId);
@@ -3061,7 +3527,10 @@ var SSEConnectionManager = class {
3061
3527
  * Complete OAuth authorization flow
3062
3528
  */
3063
3529
  async finishAuth(params) {
3064
- const { sessionId, code } = params;
3530
+ const { code } = params;
3531
+ const oauthState = params.state;
3532
+ const parsedState = parseOAuthState(oauthState);
3533
+ const sessionId = parsedState?.sessionId || oauthState;
3065
3534
  const session = await sessions.get(this.userId, sessionId);
3066
3535
  if (!session) {
3067
3536
  throw new Error("Session not found");
@@ -3086,7 +3555,7 @@ var SSEConnectionManager = class {
3086
3555
  headers: session.headers
3087
3556
  });
3088
3557
  client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3089
- await client.finishAuth(code);
3558
+ await client.finishAuth(code, oauthState);
3090
3559
  this.clients.set(sessionId, client);
3091
3560
  const tools = await client.listTools();
3092
3561
  return { success: true, toolCount: tools.tools.length };
@@ -3145,14 +3614,14 @@ var SSEConnectionManager = class {
3145
3614
  /**
3146
3615
  * Cleanup and close all connections
3147
3616
  */
3148
- dispose() {
3617
+ async dispose() {
3149
3618
  this.isActive = false;
3150
3619
  if (this.heartbeatTimer) {
3151
3620
  clearInterval(this.heartbeatTimer);
3152
3621
  }
3153
- for (const client of this.clients.values()) {
3154
- client.disconnect();
3155
- }
3622
+ await Promise.all(
3623
+ Array.from(this.clients.values()).map((client) => client.disconnect())
3624
+ );
3156
3625
  this.clients.clear();
3157
3626
  }
3158
3627
  };
@@ -3391,8 +3860,8 @@ var SSEClient = class {
3391
3860
  async getSession(sessionId) {
3392
3861
  return this.sendRequest("getSession", { sessionId });
3393
3862
  }
3394
- async finishAuth(sessionId, code) {
3395
- return this.sendRequest("finishAuth", { sessionId, code });
3863
+ async finishAuth(state, code) {
3864
+ return this.sendRequest("finishAuth", { state, code });
3396
3865
  }
3397
3866
  async listPrompts(sessionId) {
3398
3867
  return this.sendRequest("listPrompts", { sessionId });
@@ -5246,6 +5715,6 @@ function globToRegExp(pattern) {
5246
5715
  return new RegExp(regexPattern);
5247
5716
  }
5248
5717
 
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 };
5718
+ 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
5719
  //# sourceMappingURL=index.mjs.map
5251
5720
  //# sourceMappingURL=index.mjs.map