@mcp-ts/sdk 2.3.3 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +113 -30
  2. package/dist/adapters/agui-adapter.d.mts +3 -3
  3. package/dist/adapters/agui-adapter.d.ts +3 -3
  4. package/dist/adapters/agui-middleware.d.mts +3 -3
  5. package/dist/adapters/agui-middleware.d.ts +3 -3
  6. package/dist/adapters/ai-adapter.d.mts +3 -3
  7. package/dist/adapters/ai-adapter.d.ts +3 -3
  8. package/dist/adapters/langchain-adapter.d.mts +3 -3
  9. package/dist/adapters/langchain-adapter.d.ts +3 -3
  10. package/dist/adapters/mastra-adapter.d.mts +1 -1
  11. package/dist/adapters/mastra-adapter.d.ts +1 -1
  12. package/dist/client/index.d.mts +2 -2
  13. package/dist/client/index.d.ts +2 -2
  14. package/dist/client/index.js +2 -2
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/index.mjs +2 -2
  17. package/dist/client/index.mjs.map +1 -1
  18. package/dist/client/react.d.mts +9 -8
  19. package/dist/client/react.d.ts +9 -8
  20. package/dist/client/react.js +66 -26
  21. package/dist/client/react.js.map +1 -1
  22. package/dist/client/react.mjs +67 -27
  23. package/dist/client/react.mjs.map +1 -1
  24. package/dist/client/vue.d.mts +6 -5
  25. package/dist/client/vue.d.ts +6 -5
  26. package/dist/client/vue.js +27 -17
  27. package/dist/client/vue.js.map +1 -1
  28. package/dist/client/vue.mjs +27 -17
  29. package/dist/client/vue.mjs.map +1 -1
  30. package/dist/{index-Cfjsme-a.d.mts → index-ByIjEReo.d.mts} +2 -2
  31. package/dist/{index-CmjMd2ac.d.ts → index-CtXvKl8N.d.ts} +2 -2
  32. package/dist/index.d.mts +5 -5
  33. package/dist/index.d.ts +5 -5
  34. package/dist/index.js +729 -397
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +724 -396
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{multi-session-client-DYNe6az3.d.mts → multi-session-client-CIMUGF8S.d.mts} +15 -41
  39. package/dist/{multi-session-client-BYtguGJm.d.ts → multi-session-client-CnvZEGPY.d.ts} +15 -41
  40. package/dist/server/index.d.mts +45 -17
  41. package/dist/server/index.d.ts +45 -17
  42. package/dist/server/index.js +721 -391
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +718 -391
  45. package/dist/server/index.mjs.map +1 -1
  46. package/dist/shared/index.d.mts +9 -8
  47. package/dist/shared/index.d.ts +9 -8
  48. package/dist/shared/index.js +7 -5
  49. package/dist/shared/index.js.map +1 -1
  50. package/dist/shared/index.mjs +5 -4
  51. package/dist/shared/index.mjs.map +1 -1
  52. package/dist/{tool-router-BMzhoNYt.d.ts → tool-router-CZMrOG8J.d.ts} +1 -1
  53. package/dist/{tool-router-BhHsvBfP.d.mts → tool-router-CuApsDiV.d.mts} +1 -1
  54. package/dist/{types-CxFaaZrM.d.mts → types-DCk_IF4L.d.mts} +5 -3
  55. package/dist/{types-CxFaaZrM.d.ts → types-DCk_IF4L.d.ts} +5 -3
  56. package/migrations/neon/20260513010000_install_mcp_sessions.sql +40 -3
  57. package/migrations/neon/20260513020000_add_session_cleanup_cron.sql +4 -4
  58. package/migrations/supabase/20260330195700_install_mcp_sessions.sql +67 -3
  59. package/migrations/supabase/20260421010000_add_session_cleanup_cron.sql +6 -6
  60. package/package.json +13 -3
  61. package/src/client/core/sse-client.ts +2 -2
  62. package/src/client/react/index.ts +1 -1
  63. package/src/client/react/oauth-popup.tsx +34 -13
  64. package/src/client/react/use-mcp.ts +19 -45
  65. package/src/client/utils/session-state.ts +43 -0
  66. package/src/client/vue/use-mcp.ts +18 -47
  67. package/src/server/handlers/sse-handler.ts +9 -4
  68. package/src/server/mcp/multi-session-client.ts +2 -6
  69. package/src/server/mcp/oauth-client.ts +73 -220
  70. package/src/server/mcp/storage-oauth-provider.ts +79 -50
  71. package/src/server/storage/file-backend.ts +74 -18
  72. package/src/server/storage/index.ts +2 -4
  73. package/src/server/storage/memory-backend.ts +49 -13
  74. package/src/server/storage/neon-backend.ts +201 -68
  75. package/src/server/storage/redis-backend.ts +81 -23
  76. package/src/server/storage/session-lifecycle.ts +78 -0
  77. package/src/server/storage/sqlite-backend.ts +89 -15
  78. package/src/server/storage/supabase-backend.ts +188 -63
  79. package/src/server/storage/types.ts +49 -16
  80. package/src/shared/constants.ts +5 -6
  81. package/src/shared/types.ts +5 -3
  82. package/src/shared/utils.ts +26 -0
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
2
  import { customAlphabet, nanoid } from 'nanoid';
3
3
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
4
4
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
5
- import { UnauthorizedError as UnauthorizedError$1, discoverOAuthProtectedResourceMetadata, discoverAuthorizationServerMetadata, refreshAuthorization } from '@modelcontextprotocol/sdk/client/auth.js';
5
+ import { UnauthorizedError as UnauthorizedError$1 } from '@modelcontextprotocol/sdk/client/auth.js';
6
6
  import { ListToolsResultSchema, CallToolResultSchema, ListPromptsResultSchema, GetPromptResultSchema, ListResourcesResultSchema, ReadResourceResultSchema } from '@modelcontextprotocol/sdk/types.js';
7
7
  import * as fs2 from 'fs';
8
8
  import { promises } from 'fs';
@@ -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 = 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
- 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
 
@@ -1713,14 +2144,7 @@ var RpcErrorCodes = {
1713
2144
  };
1714
2145
 
1715
2146
  // src/server/mcp/oauth-client.ts
1716
- function isInvalidRefreshTokenError(error) {
1717
- if (!(error instanceof Error)) {
1718
- return false;
1719
- }
1720
- const text = `${error.name} ${error.message}`.toLowerCase();
1721
- return text.includes("invalidgrant") || text.includes("invalid_grant") || text.includes("invalid refresh token") || /refresh\s+token\s+(?:is\s+)?(?:invalid|expired|revoked)/i.test(text);
1722
- }
1723
- var MCPClient = class _MCPClient {
2147
+ var MCPClient = class {
1724
2148
  /**
1725
2149
  * Creates a new MCP client instance
1726
2150
  * Can be initialized with minimal options (userId + sessionId) for session restoration
@@ -1895,17 +2319,18 @@ var MCPClient = class _MCPClient {
1895
2319
  }
1896
2320
  this.emitStateChange("INITIALIZING");
1897
2321
  this.emitProgress("Loading session configuration...");
2322
+ let existingSession = null;
1898
2323
  if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
1899
- const sessionData = await sessions.get(this.userId, this.sessionId);
1900
- if (!sessionData) {
2324
+ existingSession = await sessions.get(this.userId, this.sessionId);
2325
+ if (!existingSession) {
1901
2326
  throw new Error(`Session not found: ${this.sessionId}`);
1902
2327
  }
1903
- this.serverUrl = this.serverUrl || sessionData.serverUrl;
1904
- this.callbackUrl = this.callbackUrl || sessionData.callbackUrl;
1905
- this.serverName = this.serverName || sessionData.serverName;
1906
- this.serverId = this.serverId || sessionData.serverId || "unknown";
1907
- this.headers = this.headers || sessionData.headers;
1908
- this.createdAt = sessionData.createdAt;
2328
+ this.serverUrl = this.serverUrl || existingSession.serverUrl;
2329
+ this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
2330
+ this.serverName = this.serverName || existingSession.serverName;
2331
+ this.serverId = this.serverId || existingSession.serverId || "unknown";
2332
+ this.headers = this.headers || existingSession.headers;
2333
+ this.createdAt = existingSession.createdAt;
1909
2334
  }
1910
2335
  if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
1911
2336
  throw new Error("Missing required connection metadata");
@@ -1949,10 +2374,13 @@ var MCPClient = class _MCPClient {
1949
2374
  }
1950
2375
  );
1951
2376
  }
1952
- const existingSession = await sessions.get(this.userId, this.sessionId);
2377
+ if (existingSession === null) {
2378
+ existingSession = await sessions.get(this.userId, this.sessionId);
2379
+ }
1953
2380
  if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
1954
2381
  this.createdAt = Date.now();
1955
- console.log(`[MCPClient] Creating initial session ${this.sessionId} for OAuth flow`);
2382
+ const updatedAt = this.createdAt;
2383
+ console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
1956
2384
  await sessions.create({
1957
2385
  sessionId: this.sessionId,
1958
2386
  userId: this.userId,
@@ -1963,18 +2391,18 @@ var MCPClient = class _MCPClient {
1963
2391
  transportType: this.transportType || "streamable-http",
1964
2392
  headers: this.headers,
1965
2393
  createdAt: this.createdAt,
1966
- active: false
1967
- }, Math.floor(STATE_EXPIRATION_MS / 1e3));
2394
+ updatedAt,
2395
+ status: "pending"
2396
+ });
1968
2397
  }
1969
2398
  }
1970
2399
  /**
1971
2400
  * Saves current session state to the session store
1972
2401
  * Creates new session if it doesn't exist, updates if it does
1973
- * @param ttl - Time-to-live in seconds (defaults to 12hr for connected sessions)
1974
- * @param active - Session status marker used to avoid unnecessary TTL rewrites
2402
+ * @param status - Session lifecycle status used by storage cleanup
1975
2403
  * @private
1976
2404
  */
1977
- async saveSession(ttl = SESSION_TTL_SECONDS, active = true) {
2405
+ async saveSession(status = "active", existingSession) {
1978
2406
  if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
1979
2407
  return;
1980
2408
  }
@@ -1988,13 +2416,29 @@ var MCPClient = class _MCPClient {
1988
2416
  transportType: this.transportType || "streamable-http",
1989
2417
  headers: this.headers,
1990
2418
  createdAt: this.createdAt || Date.now(),
1991
- active
2419
+ updatedAt: Date.now(),
2420
+ status
1992
2421
  };
1993
- const existingSession = await sessions.get(this.userId, this.sessionId);
2422
+ if (status === "active") {
2423
+ sessionData.authUrl = null;
2424
+ }
2425
+ if (existingSession === void 0) {
2426
+ existingSession = await sessions.get(this.userId, this.sessionId);
2427
+ }
1994
2428
  if (existingSession) {
1995
- await sessions.update(this.userId, this.sessionId, sessionData, ttl);
2429
+ await sessions.update(this.userId, this.sessionId, sessionData);
1996
2430
  } else {
1997
- await sessions.create(sessionData, ttl);
2431
+ await sessions.create(sessionData);
2432
+ }
2433
+ }
2434
+ /**
2435
+ * Removes transient setup/auth sessions without masking the original error.
2436
+ * @private
2437
+ */
2438
+ async deleteTransientSession() {
2439
+ try {
2440
+ await sessions.delete(this.userId, this.sessionId);
2441
+ } catch {
1998
2442
  }
1999
2443
  }
2000
2444
  /**
@@ -2054,15 +2498,13 @@ var MCPClient = class _MCPClient {
2054
2498
  throw new Error(error);
2055
2499
  }
2056
2500
  try {
2057
- this.emitProgress("Validating OAuth tokens...");
2058
- await this.getValidTokens();
2059
2501
  this.emitStateChange("CONNECTING");
2060
2502
  const { transportType } = await this.tryConnect();
2061
2503
  this.transportType = transportType;
2062
2504
  this.emitStateChange("CONNECTED");
2063
2505
  this.emitProgress("Connected successfully");
2064
- console.log(`[MCPClient] Saving session ${this.sessionId} with 12hr TTL (connect success)`);
2065
- await this.saveSession(SESSION_TTL_SECONDS, true);
2506
+ console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
2507
+ await this.saveSession("active");
2066
2508
  } catch (error) {
2067
2509
  if (error instanceof UnauthorizedError$1 || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
2068
2510
  let authUrl = "";
@@ -2074,15 +2516,12 @@ var MCPClient = class _MCPClient {
2074
2516
  const message = detail.toLowerCase() === "unauthorized" ? "OAuth authorization URL not available" : `OAuth authorization URL not available: ${detail}`;
2075
2517
  this.emitError(message, "auth");
2076
2518
  this.emitStateChange("FAILED");
2077
- try {
2078
- await sessions.delete(this.userId, this.sessionId);
2079
- } catch {
2080
- }
2519
+ await this.deleteTransientSession();
2081
2520
  throw new Error(message);
2082
2521
  }
2083
2522
  this.emitStateChange("AUTHENTICATING");
2084
- console.log(`[MCPClient] Saving session ${this.sessionId} with 10min TTL (OAuth pending)`);
2085
- await this.saveSession(Math.floor(STATE_EXPIRATION_MS / 1e3), false);
2523
+ console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
2524
+ await this.saveSession("pending");
2086
2525
  if (this.serverId) {
2087
2526
  this._onConnectionEvent.fire({
2088
2527
  type: "auth_required",
@@ -2102,7 +2541,7 @@ var MCPClient = class _MCPClient {
2102
2541
  this.emitStateChange("FAILED");
2103
2542
  try {
2104
2543
  const existingSession = await sessions.get(this.userId, this.sessionId);
2105
- if (!existingSession || existingSession.active !== true) {
2544
+ if (!existingSession || existingSession.status !== "active") {
2106
2545
  await sessions.delete(this.userId, this.sessionId);
2107
2546
  }
2108
2547
  } catch {
@@ -2117,7 +2556,7 @@ var MCPClient = class _MCPClient {
2117
2556
  * @param authCode - Authorization code received from OAuth callback
2118
2557
  */
2119
2558
  // TODO: needs to be optimized
2120
- async finishAuth(authCode) {
2559
+ async finishAuth(authCode, state) {
2121
2560
  this.emitStateChange("AUTHENTICATING");
2122
2561
  this.emitProgress("Exchanging authorization code for tokens...");
2123
2562
  await this.initialize();
@@ -2127,6 +2566,16 @@ var MCPClient = class _MCPClient {
2127
2566
  this.emitStateChange("FAILED");
2128
2567
  throw new Error(error);
2129
2568
  }
2569
+ if (state) {
2570
+ const stateCheck = await this.oauthProvider.checkState(state);
2571
+ if (!stateCheck.valid) {
2572
+ const error = stateCheck.error || "Invalid OAuth state";
2573
+ this.emitError(error, "auth");
2574
+ this.emitStateChange("FAILED");
2575
+ throw new Error(error);
2576
+ }
2577
+ await this.oauthProvider.consumeState(state);
2578
+ }
2130
2579
  const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
2131
2580
  let lastError;
2132
2581
  let tokensExchanged = false;
@@ -2166,8 +2615,8 @@ var MCPClient = class _MCPClient {
2166
2615
  await this.client.connect(this.transport);
2167
2616
  this.transportType = currentType;
2168
2617
  this.emitStateChange("CONNECTED");
2169
- console.log(`[MCPClient] Updating session ${this.sessionId} to 12hr TTL (OAuth complete)`);
2170
- await this.saveSession(SESSION_TTL_SECONDS, true);
2618
+ console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
2619
+ await this.saveSession("active");
2171
2620
  return;
2172
2621
  } catch (error) {
2173
2622
  lastError = error;
@@ -2180,12 +2629,14 @@ var MCPClient = class _MCPClient {
2180
2629
  const msg = error instanceof Error ? error.message : "Authentication failed";
2181
2630
  this.emitError(msg, "auth");
2182
2631
  this.emitStateChange("FAILED");
2632
+ await this.deleteTransientSession();
2183
2633
  throw error;
2184
2634
  }
2185
2635
  if (isLastAttempt) {
2186
2636
  const msg = error instanceof Error ? error.message : "Authentication failed";
2187
2637
  this.emitError(msg, "auth");
2188
2638
  this.emitStateChange("FAILED");
2639
+ await this.deleteTransientSession();
2189
2640
  throw error;
2190
2641
  }
2191
2642
  this.emitProgress(`Auth attempt with ${currentType} failed: ${errorMessage}. Retrying...`);
@@ -2195,6 +2646,7 @@ var MCPClient = class _MCPClient {
2195
2646
  const errorMessage = lastError instanceof Error ? lastError.message : "Authentication failed";
2196
2647
  this.emitError(errorMessage, "auth");
2197
2648
  this.emitStateChange("FAILED");
2649
+ await this.deleteTransientSession();
2198
2650
  throw lastError;
2199
2651
  }
2200
2652
  }
@@ -2380,67 +2832,6 @@ var MCPClient = class _MCPClient {
2380
2832
  };
2381
2833
  return await this.client.request(request, ReadResourceResultSchema);
2382
2834
  }
2383
- /**
2384
- * Refreshes the OAuth access token using the refresh token
2385
- * Discovers OAuth metadata from server and exchanges refresh token for new access token
2386
- * @returns True if refresh was successful, false otherwise
2387
- */
2388
- async refreshToken() {
2389
- await this.initialize();
2390
- if (!this.oauthProvider) {
2391
- return false;
2392
- }
2393
- const tokens = await this.oauthProvider.tokens();
2394
- if (!tokens || !tokens.refresh_token) {
2395
- return false;
2396
- }
2397
- const clientInformation = await this.oauthProvider.clientInformation();
2398
- if (!clientInformation) {
2399
- return false;
2400
- }
2401
- try {
2402
- const resourceMetadata = await discoverOAuthProtectedResourceMetadata(this.serverUrl);
2403
- const authServerUrl = resourceMetadata?.authorization_servers?.[0] || this.serverUrl;
2404
- const authMetadata = await discoverAuthorizationServerMetadata(authServerUrl);
2405
- const newTokens = await refreshAuthorization(authServerUrl, {
2406
- metadata: authMetadata,
2407
- clientInformation,
2408
- refreshToken: tokens.refresh_token
2409
- });
2410
- await this.oauthProvider.saveTokens(newTokens);
2411
- return true;
2412
- } catch (error) {
2413
- console.error("[OAuth] Token refresh failed:", error);
2414
- if (isInvalidRefreshTokenError(error)) {
2415
- try {
2416
- await this.oauthProvider.invalidateCredentials?.("tokens");
2417
- this.emitProgress("OAuth refresh token is invalid; requesting reauthorization...");
2418
- } catch (invalidateError) {
2419
- console.warn("[OAuth] Failed to invalidate stale refresh token credentials:", invalidateError);
2420
- }
2421
- }
2422
- return false;
2423
- }
2424
- }
2425
- /**
2426
- * Ensures OAuth tokens are valid, refreshing them if expired
2427
- * Called automatically by connect() - rarely needs to be called manually
2428
- * @returns True if valid tokens are available, false otherwise
2429
- */
2430
- async getValidTokens() {
2431
- await this.initialize();
2432
- if (!this.oauthProvider) {
2433
- return false;
2434
- }
2435
- const tokens = await this.oauthProvider.tokens();
2436
- if (!tokens) {
2437
- return false;
2438
- }
2439
- if (this.oauthProvider.isTokenExpired()) {
2440
- return await this.refreshToken();
2441
- }
2442
- return true;
2443
- }
2444
2835
  /**
2445
2836
  * Reconnects to MCP server using existing OAuth provider from Redis
2446
2837
  * Used for session restoration in serverless environments
@@ -2570,68 +2961,6 @@ var MCPClient = class _MCPClient {
2570
2961
  getSessionId() {
2571
2962
  return this.sessionId;
2572
2963
  }
2573
- /**
2574
- * Gets MCP server configuration for all active user sessions
2575
- * Loads sessions from Redis, validates OAuth tokens, refreshes if expired
2576
- * Returns ready-to-use configuration with valid auth headers
2577
- * @param userId - User ID to fetch sessions for
2578
- * @returns Object keyed by sanitized server labels containing transport, url, headers, etc.
2579
- * @static
2580
- */
2581
- static async getMcpServerConfig(userId) {
2582
- const mcpConfig = {};
2583
- const sessionList = await sessions.list(userId);
2584
- await Promise.all(
2585
- sessionList.map(async (sessionData) => {
2586
- const { sessionId } = sessionData;
2587
- try {
2588
- if (!sessionData.serverId || !sessionData.transportType || !sessionData.serverUrl || !sessionData.callbackUrl) {
2589
- await sessions.delete(userId, sessionId);
2590
- return;
2591
- }
2592
- let headers;
2593
- try {
2594
- const client = new _MCPClient({
2595
- userId,
2596
- sessionId,
2597
- serverId: sessionData.serverId,
2598
- serverUrl: sessionData.serverUrl,
2599
- callbackUrl: sessionData.callbackUrl,
2600
- serverName: sessionData.serverName,
2601
- transportType: sessionData.transportType,
2602
- headers: sessionData.headers
2603
- });
2604
- await client.initialize();
2605
- const hasValidTokens = await client.getValidTokens();
2606
- if (hasValidTokens && client.oauthProvider) {
2607
- const tokens = await client.oauthProvider.tokens();
2608
- if (tokens?.access_token) {
2609
- headers = { Authorization: `Bearer ${tokens.access_token}` };
2610
- }
2611
- }
2612
- } catch (error) {
2613
- console.warn(`[MCP] Failed to get OAuth tokens for ${sessionId}:`, error);
2614
- }
2615
- const label = sanitizeServerLabel(
2616
- sessionData.serverName || sessionData.serverId || "server"
2617
- );
2618
- mcpConfig[label] = {
2619
- transport: sessionData.transportType,
2620
- url: sessionData.serverUrl,
2621
- ...sessionData.serverName && {
2622
- serverName: sessionData.serverName,
2623
- serverLabel: label
2624
- },
2625
- ...headers && { headers }
2626
- };
2627
- } catch (error) {
2628
- await sessions.delete(userId, sessionId);
2629
- console.warn(`[MCP] Failed to process session ${sessionId}:`, error);
2630
- }
2631
- })
2632
- );
2633
- return mcpConfig;
2634
- }
2635
2964
  };
2636
2965
 
2637
2966
  // src/server/mcp/multi-session-client.ts
@@ -2666,18 +2995,13 @@ var MultiSessionClient = class {
2666
2995
  *
2667
2996
  * A session is considered connectable when:
2668
2997
  * - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
2669
- * - Its `active` flag is not explicitly `false` sessions with `active: false` are
2670
- * either mid-OAuth flow, auth-pending, or previously failed. We skip those here
2998
+ * - Its status is `active`. Pending sessions are skipped here
2671
2999
  * and let the OAuth flow complete separately before we try to reconnect them.
2672
- *
2673
- * Note: Sessions where `active` is `undefined` (legacy records) are included
2674
- * for backwards compatibility.
2675
3000
  */
2676
3001
  async getActiveSessions() {
2677
3002
  const sessionList = await sessions.list(this.userId);
2678
3003
  const valid = sessionList.filter(
2679
- (s) => s.serverId && s.serverUrl && s.callbackUrl && s.active !== false
2680
- // exclude OAuth-pending / failed sessions
3004
+ (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
2681
3005
  );
2682
3006
  return valid;
2683
3007
  }
@@ -2948,7 +3272,8 @@ var SSEConnectionManager = class {
2948
3272
  serverUrl: s.serverUrl,
2949
3273
  transport: s.transportType,
2950
3274
  createdAt: s.createdAt,
2951
- active: s.active !== false
3275
+ updatedAt: s.updatedAt ?? s.createdAt,
3276
+ status: s.status ?? "pending"
2952
3277
  }))
2953
3278
  };
2954
3279
  }
@@ -2964,7 +3289,7 @@ var SSEConnectionManager = class {
2964
3289
  (s) => s.serverId === serverId || s.serverUrl === serverUrl
2965
3290
  );
2966
3291
  if (duplicate) {
2967
- if (duplicate.active === false) {
3292
+ if (duplicate.status === "pending") {
2968
3293
  await this.getSession({ sessionId: duplicate.sessionId });
2969
3294
  return {
2970
3295
  sessionId: duplicate.sessionId,
@@ -3151,7 +3476,10 @@ var SSEConnectionManager = class {
3151
3476
  * Complete OAuth authorization flow
3152
3477
  */
3153
3478
  async finishAuth(params) {
3154
- const { sessionId, code } = params;
3479
+ const { code } = params;
3480
+ const oauthState = params.state;
3481
+ const parsedState = parseOAuthState(oauthState);
3482
+ const sessionId = parsedState?.sessionId || oauthState;
3155
3483
  const session = await sessions.get(this.userId, sessionId);
3156
3484
  if (!session) {
3157
3485
  throw new Error("Session not found");
@@ -3176,7 +3504,7 @@ var SSEConnectionManager = class {
3176
3504
  headers: session.headers
3177
3505
  });
3178
3506
  client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3179
- await client.finishAuth(code);
3507
+ await client.finishAuth(code, oauthState);
3180
3508
  this.clients.set(sessionId, client);
3181
3509
  const tools = await client.listTools();
3182
3510
  return { success: true, toolCount: tools.tools.length };
@@ -3481,8 +3809,8 @@ var SSEClient = class {
3481
3809
  async getSession(sessionId) {
3482
3810
  return this.sendRequest("getSession", { sessionId });
3483
3811
  }
3484
- async finishAuth(sessionId, code) {
3485
- return this.sendRequest("finishAuth", { sessionId, code });
3812
+ async finishAuth(state, code) {
3813
+ return this.sendRequest("finishAuth", { state, code });
3486
3814
  }
3487
3815
  async listPrompts(sessionId) {
3488
3816
  return this.sendRequest("listPrompts", { sessionId });
@@ -5336,6 +5664,6 @@ function globToRegExp(pattern) {
5336
5664
  return new RegExp(regexPattern);
5337
5665
  }
5338
5666
 
5339
- export { APP_HOST_DEFAULTS, AppHost, AuthenticationError, ConfigurationError, ConnectionError, DEFAULT_CLIENT_NAME, DEFAULT_CLIENT_URI, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_LOGO_URI, DEFAULT_MCP_APP_CSP, DEFAULT_POLICY_URI, DisposableStore, Emitter, InvalidStateError, MCPClient, MCP_CLIENT_NAME, MCP_CLIENT_VERSION, McpError, MultiSessionClient, NotConnectedError, REDIS_KEY_PREFIX, RpcErrorCodes, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, SESSION_TTL_SECONDS, SOFTWARE_ID, SOFTWARE_VERSION, SSEClient, SSEConnectionManager, STATE_EXPIRATION_MS, SchemaCompressor, SessionNotFoundError, SessionValidationError, StorageOAuthClientProvider, TOKEN_EXPIRY_BUFFER_MS, ToolExecutionError, ToolIndex, ToolRouter, UnauthorizedError, createExecuteToolDefinition, createGetSchemaToolDefinition, createListServersToolDefinition, createNextMcpHandler, createRegexSearchToolDefinition, createSSEHandler, createSearchToolDefinition, executeMetaTool, findToolByName, getToolUiResourceUri, isCallToolSuccess, isConnectAuthRequired, isConnectError, isConnectSuccess, isListToolsSuccess, isMetaTool, onSessionMutation, resolveMetaToolProxy, sanitizeServerLabel, sessions };
5667
+ export { APP_HOST_DEFAULTS, AppHost, AuthenticationError, ConfigurationError, ConnectionError, DEFAULT_CLIENT_NAME, DEFAULT_CLIENT_URI, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_LOGO_URI, DEFAULT_MCP_APP_CSP, DEFAULT_POLICY_URI, DORMANT_SESSION_EXPIRATION_MS, DORMANT_SESSION_EXPIRATION_SECONDS, DisposableStore, Emitter, InvalidStateError, MCPClient, MCP_CLIENT_NAME, MCP_CLIENT_VERSION, McpError, MultiSessionClient, NotConnectedError, PENDING_SESSION_EXPIRATION_SECONDS, REDIS_KEY_PREFIX, RpcErrorCodes, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, SOFTWARE_ID, SOFTWARE_VERSION, SSEClient, SSEConnectionManager, STATE_EXPIRATION_MS, SchemaCompressor, SessionNotFoundError, SessionValidationError, StorageOAuthClientProvider, ToolExecutionError, ToolIndex, ToolRouter, UnauthorizedError, createExecuteToolDefinition, createGetSchemaToolDefinition, createListServersToolDefinition, createNextMcpHandler, createRegexSearchToolDefinition, createSSEHandler, createSearchToolDefinition, executeMetaTool, findToolByName, getToolUiResourceUri, isCallToolSuccess, isConnectAuthRequired, isConnectError, isConnectSuccess, isListToolsSuccess, isMetaTool, onSessionMutation, resolveMetaToolProxy, sanitizeServerLabel, sessions };
5340
5668
  //# sourceMappingURL=index.mjs.map
5341
5669
  //# sourceMappingURL=index.mjs.map