@mcp-ts/sdk 2.5.3 → 2.6.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 (73) hide show
  1. package/README.md +3 -574
  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 +3 -3
  11. package/dist/adapters/mastra-adapter.d.ts +3 -3
  12. package/dist/client/index.d.mts +2 -2
  13. package/dist/client/index.d.ts +2 -2
  14. package/dist/client/index.js +3 -0
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/index.mjs +3 -0
  17. package/dist/client/index.mjs.map +1 -1
  18. package/dist/client/react.d.mts +10 -18
  19. package/dist/client/react.d.ts +10 -18
  20. package/dist/client/react.js +17 -0
  21. package/dist/client/react.js.map +1 -1
  22. package/dist/client/react.mjs +17 -0
  23. package/dist/client/react.mjs.map +1 -1
  24. package/dist/client/vue.d.mts +4 -4
  25. package/dist/client/vue.d.ts +4 -4
  26. package/dist/client/vue.js +3 -0
  27. package/dist/client/vue.js.map +1 -1
  28. package/dist/client/vue.mjs +3 -0
  29. package/dist/client/vue.mjs.map +1 -1
  30. package/dist/{index-BRZO_JyI.d.mts → index-BrDkbut5.d.ts} +2 -1
  31. package/dist/{index-C3YGagi2.d.ts → index-DHQvlx4K.d.mts} +2 -1
  32. package/dist/index.d.mts +5 -5
  33. package/dist/index.d.ts +5 -5
  34. package/dist/index.js +1056 -876
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +1048 -871
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{multi-session-client-CuacvZaQ.d.ts → multi-session-client-CQzymvHl.d.ts} +183 -227
  39. package/dist/{multi-session-client-DqzT8Oo7.d.mts → multi-session-client-C_Ja2TuV.d.mts} +183 -227
  40. package/dist/server/index.d.mts +338 -88
  41. package/dist/server/index.d.ts +338 -88
  42. package/dist/server/index.js +1053 -876
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +1045 -871
  45. package/dist/server/index.mjs.map +1 -1
  46. package/dist/shared/index.d.mts +4 -4
  47. package/dist/shared/index.d.ts +4 -4
  48. package/dist/shared/index.js.map +1 -1
  49. package/dist/shared/index.mjs.map +1 -1
  50. package/dist/{tool-router-B0qpui-V.d.mts → tool-router-C1n7OXbl.d.mts} +1 -1
  51. package/dist/{tool-router-2qUyZ-cP.d.ts → tool-router-CBIawFnt.d.ts} +1 -1
  52. package/dist/{types-BkJ_rgzo.d.mts → types-DX71u9gR.d.mts} +11 -5
  53. package/dist/{types-BkJ_rgzo.d.ts → types-DX71u9gR.d.ts} +11 -5
  54. package/migrations/neon/20260513010000_install_mcp_sessions.sql +4 -34
  55. package/migrations/supabase/20260330195700_install_mcp_sessions.sql +5 -62
  56. package/package.json +2 -1
  57. package/src/client/core/sse-client.ts +5 -0
  58. package/src/client/react/use-mcp.ts +27 -28
  59. package/src/server/handlers/sse-handler.ts +671 -633
  60. package/src/server/index.ts +2 -1
  61. package/src/server/mcp/multi-session-client.ts +34 -10
  62. package/src/server/mcp/oauth-client.ts +197 -205
  63. package/src/server/mcp/storage-oauth-provider.ts +148 -41
  64. package/src/server/mcp/tool-policy-gateway.ts +11 -1
  65. package/src/server/storage/file-backend.ts +25 -21
  66. package/src/server/storage/index.ts +71 -0
  67. package/src/server/storage/memory-backend.ts +24 -12
  68. package/src/server/storage/neon-backend.ts +70 -72
  69. package/src/server/storage/redis-backend.ts +26 -36
  70. package/src/server/storage/sqlite-backend.ts +25 -39
  71. package/src/server/storage/supabase-backend.ts +45 -54
  72. package/src/server/storage/types.ts +22 -2
  73. package/src/shared/types.ts +13 -3
@@ -4,6 +4,7 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
4
4
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
5
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
+ import { AsyncLocalStorage } from 'async_hooks';
7
8
  import * as fs2 from 'fs';
8
9
  import { promises } from 'fs';
9
10
  import * as path from 'path';
@@ -273,7 +274,6 @@ var RedisStorageBackend = class {
273
274
  constructor(redis2) {
274
275
  this.redis = redis2;
275
276
  __publicField(this, "KEY_PREFIX", "mcp:session:");
276
- __publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
277
277
  __publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
278
278
  __publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
279
279
  }
@@ -292,9 +292,6 @@ var RedisStorageBackend = class {
292
292
  getSessionKey(userId, sessionId) {
293
293
  return `${this.KEY_PREFIX}${userId}:${sessionId}`;
294
294
  }
295
- getCredentialsKey(userId, sessionId) {
296
- return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
297
- }
298
295
  /**
299
296
  * Generates Redis key for tracking all sessions for a user
300
297
  * @private
@@ -384,48 +381,54 @@ var RedisStorageBackend = class {
384
381
  }
385
382
  async patchCredentials(userId, sessionId, data) {
386
383
  const sessionKey = this.getSessionKey(userId, sessionId);
387
- const credentialsKey = this.getCredentialsKey(userId, sessionId);
388
- const sessionExists = await this.redis.exists(sessionKey);
389
- if (!sessionExists) {
384
+ const currentStr = await this.redis.get(sessionKey);
385
+ if (!currentStr) {
390
386
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
391
387
  }
392
- const session = await this.get(userId, sessionId);
393
- const currentTtl = await this.redis.ttl(sessionKey);
394
- const effectiveTtl = currentTtl > 0 && session ? currentTtl : resolveSessionRedisTtlSeconds(session ?? { status: "pending" });
395
- const currentStr = await this.redis.get(credentialsKey);
396
- const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
397
- const credentials = { ...current, ...data, sessionId, userId };
398
- if (effectiveTtl > 0) {
399
- await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
400
- } else {
401
- await this.redis.set(credentialsKey, JSON.stringify(credentials));
402
- }
388
+ const current = JSON.parse(currentStr);
389
+ const updated = { ...current, ...data, sessionId, userId };
390
+ const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
391
+ await this.redis.set(sessionKey, JSON.stringify(updated), "EX", effectiveTtl);
403
392
  }
404
- async get(userId, sessionId) {
393
+ async get(userId, sessionId, options) {
405
394
  try {
406
395
  const sessionKey = this.getSessionKey(userId, sessionId);
407
396
  const sessionDataStr = await this.redis.get(sessionKey);
408
- if (!sessionDataStr) {
409
- return null;
397
+ if (!sessionDataStr) return null;
398
+ const session = normalizeStoredSession(JSON.parse(sessionDataStr));
399
+ if (!options?.includeCredentials) {
400
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
401
+ return sessionOnly;
410
402
  }
411
- const session = JSON.parse(sessionDataStr);
412
- return normalizeStoredSession(session);
403
+ return session;
413
404
  } catch (error) {
414
405
  console.error("[RedisStorageBackend] Failed to get session:", error);
415
406
  return null;
416
407
  }
417
408
  }
418
409
  async getCredentials(userId, sessionId) {
419
- const session = await this.get(userId, sessionId);
410
+ const session = await this.get(userId, sessionId, { includeCredentials: true });
420
411
  if (!session) return null;
421
- const credentialsStr = await this.redis.get(this.getCredentialsKey(userId, sessionId));
422
- return credentialsStr ? JSON.parse(credentialsStr) : { sessionId, userId };
412
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
413
+ return {
414
+ sessionId,
415
+ userId,
416
+ clientInformation,
417
+ tokens,
418
+ codeVerifier,
419
+ codeVerifierChallenge,
420
+ codeVerifierNonce,
421
+ clientId,
422
+ oauthState
423
+ };
423
424
  }
424
425
  async clearCredentials(userId, sessionId) {
425
426
  await this.patchCredentials(userId, sessionId, {
426
427
  clientInformation: null,
427
428
  tokens: null,
428
429
  codeVerifier: null,
430
+ codeVerifierChallenge: null,
431
+ codeVerifierNonce: null,
429
432
  clientId: null,
430
433
  oauthState: null
431
434
  });
@@ -458,10 +461,9 @@ var RedisStorageBackend = class {
458
461
  async delete(userId, sessionId) {
459
462
  try {
460
463
  const sessionKey = this.getSessionKey(userId, sessionId);
461
- const credentialsKey = this.getCredentialsKey(userId, sessionId);
462
464
  const userIdKey = this.getUserIdKey(userId);
463
465
  await this.redis.srem(userIdKey, sessionId);
464
- await this.redis.del(sessionKey, credentialsKey);
466
+ await this.redis.del(sessionKey);
465
467
  } catch (error) {
466
468
  console.error("[RedisStorageBackend] Failed to remove session:", error);
467
469
  }
@@ -492,9 +494,8 @@ var RedisStorageBackend = class {
492
494
  async clearAll() {
493
495
  try {
494
496
  const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
495
- const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
496
497
  const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
497
- const allKeys = [...keys, ...credentialKeys, ...userIdKeys];
498
+ const allKeys = [...keys, ...userIdKeys];
498
499
  if (allKeys.length > 0) {
499
500
  await this.redis.del(...allKeys);
500
501
  }
@@ -518,7 +519,6 @@ var RedisStorageBackend = class {
518
519
  const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
519
520
  if (staleSessionIds.length > 0) {
520
521
  await this.redis.srem(userIdKey, ...staleSessionIds);
521
- await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
522
522
  }
523
523
  const remainingCount = await this.redis.scard(userIdKey);
524
524
  if (remainingCount === 0) {
@@ -543,7 +543,6 @@ var MemoryStorageBackend = class {
543
543
  constructor() {
544
544
  // Map<userId:sessionId, Session>
545
545
  __publicField(this, "sessions", /* @__PURE__ */ new Map());
546
- __publicField(this, "credentials", /* @__PURE__ */ new Map());
547
546
  // Map<userId, Set<sessionId>>
548
547
  __publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
549
548
  }
@@ -581,26 +580,46 @@ var MemoryStorageBackend = class {
581
580
  }
582
581
  async patchCredentials(userId, sessionId, data) {
583
582
  const sessionKey = this.getSessionKey(userId, sessionId);
584
- if (!this.sessions.has(sessionKey)) {
583
+ const current = this.sessions.get(sessionKey);
584
+ if (!current) {
585
585
  throw new Error(`Session ${sessionId} not found`);
586
586
  }
587
- const current = this.credentials.get(sessionKey) ?? { sessionId, userId };
588
- this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
587
+ this.sessions.set(sessionKey, { ...current, ...data, sessionId, userId });
589
588
  }
590
- async get(userId, sessionId) {
589
+ async get(userId, sessionId, options) {
591
590
  const sessionKey = this.getSessionKey(userId, sessionId);
592
- return this.sessions.get(sessionKey) || null;
591
+ const session = this.sessions.get(sessionKey) || null;
592
+ if (!session) return session;
593
+ if (!options?.includeCredentials) {
594
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
595
+ return sessionOnly;
596
+ }
597
+ return session;
593
598
  }
594
599
  async getCredentials(userId, sessionId) {
595
600
  const sessionKey = this.getSessionKey(userId, sessionId);
596
- if (!this.sessions.has(sessionKey)) return null;
597
- return this.credentials.get(sessionKey) ?? { sessionId, userId };
601
+ const session = this.sessions.get(sessionKey);
602
+ if (!session) return null;
603
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
604
+ return {
605
+ sessionId,
606
+ userId,
607
+ clientInformation,
608
+ tokens,
609
+ codeVerifier,
610
+ codeVerifierChallenge,
611
+ codeVerifierNonce,
612
+ clientId,
613
+ oauthState
614
+ };
598
615
  }
599
616
  async clearCredentials(userId, sessionId) {
600
617
  await this.patchCredentials(userId, sessionId, {
601
618
  clientInformation: null,
602
619
  tokens: null,
603
620
  codeVerifier: null,
621
+ codeVerifierChallenge: null,
622
+ codeVerifierNonce: null,
604
623
  clientId: null,
605
624
  oauthState: null
606
625
  });
@@ -624,7 +643,6 @@ var MemoryStorageBackend = class {
624
643
  async delete(userId, sessionId) {
625
644
  const sessionKey = this.getSessionKey(userId, sessionId);
626
645
  this.sessions.delete(sessionKey);
627
- this.credentials.delete(sessionKey);
628
646
  const set = this.userIdSessions.get(userId);
629
647
  if (set) {
630
648
  set.delete(sessionId);
@@ -638,14 +656,12 @@ var MemoryStorageBackend = class {
638
656
  }
639
657
  async clearAll() {
640
658
  this.sessions.clear();
641
- this.credentials.clear();
642
659
  this.userIdSessions.clear();
643
660
  }
644
661
  async cleanupExpired() {
645
662
  for (const [key, session] of this.sessions.entries()) {
646
663
  if (!isSessionExpired(session)) continue;
647
664
  this.sessions.delete(key);
648
- this.credentials.delete(key);
649
665
  const set = this.userIdSessions.get(session.userId);
650
666
  if (set) {
651
667
  set.delete(session.sessionId);
@@ -665,7 +681,6 @@ var FileStorageBackend = class {
665
681
  constructor(options = {}) {
666
682
  __publicField(this, "filePath");
667
683
  __publicField(this, "memoryCache", null);
668
- __publicField(this, "credentialsCache", null);
669
684
  __publicField(this, "initialized", false);
670
685
  this.filePath = options.path || "./sessions.json";
671
686
  }
@@ -680,22 +695,15 @@ var FileStorageBackend = class {
680
695
  const data = await promises.readFile(this.filePath, "utf-8");
681
696
  const json = JSON.parse(data);
682
697
  this.memoryCache = /* @__PURE__ */ new Map();
683
- this.credentialsCache = /* @__PURE__ */ new Map();
684
698
  if (Array.isArray(json.sessions)) {
685
699
  json.sessions.forEach((s) => {
686
700
  const session = normalizeStoredSession(s);
687
701
  this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
688
702
  });
689
703
  }
690
- if (Array.isArray(json.credentials)) {
691
- json.credentials.forEach((c) => {
692
- this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
693
- });
694
- }
695
704
  } catch (error) {
696
705
  if (error.code === "ENOENT") {
697
706
  this.memoryCache = /* @__PURE__ */ new Map();
698
- this.credentialsCache = /* @__PURE__ */ new Map();
699
707
  await this.flush();
700
708
  } else {
701
709
  console.error("[FileStorage] Failed to load sessions:", error);
@@ -709,10 +717,9 @@ var FileStorageBackend = class {
709
717
  if (!this.initialized) await this.init();
710
718
  }
711
719
  async flush() {
712
- if (!this.memoryCache || !this.credentialsCache) return;
720
+ if (!this.memoryCache) return;
713
721
  await promises.writeFile(this.filePath, JSON.stringify({
714
- sessions: Array.from(this.memoryCache.values()),
715
- credentials: Array.from(this.credentialsCache.values())
722
+ sessions: Array.from(this.memoryCache.values())
716
723
  }, null, 2), "utf-8");
717
724
  }
718
725
  getSessionKey(userId, sessionId) {
@@ -747,29 +754,49 @@ var FileStorageBackend = class {
747
754
  async patchCredentials(userId, sessionId, data) {
748
755
  await this.ensureInitialized();
749
756
  const sessionKey = this.getSessionKey(userId, sessionId);
750
- if (!this.memoryCache.has(sessionKey)) {
757
+ const current = this.memoryCache.get(sessionKey);
758
+ if (!current) {
751
759
  throw new Error(`Session ${sessionId} not found`);
752
760
  }
753
- const current = this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
754
- this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
761
+ this.memoryCache.set(sessionKey, { ...current, ...data, sessionId, userId });
755
762
  await this.flush();
756
763
  }
757
- async get(userId, sessionId) {
764
+ async get(userId, sessionId, options) {
758
765
  await this.ensureInitialized();
759
766
  const sessionKey = this.getSessionKey(userId, sessionId);
760
- return this.memoryCache.get(sessionKey) || null;
767
+ const session = this.memoryCache.get(sessionKey) || null;
768
+ if (!session) return null;
769
+ if (!options?.includeCredentials) {
770
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
771
+ return sessionOnly;
772
+ }
773
+ return session;
761
774
  }
762
775
  async getCredentials(userId, sessionId) {
763
776
  await this.ensureInitialized();
764
777
  const sessionKey = this.getSessionKey(userId, sessionId);
765
- if (!this.memoryCache.has(sessionKey)) return null;
766
- return this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
778
+ const session = this.memoryCache.get(sessionKey);
779
+ if (!session) return null;
780
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
781
+ return {
782
+ sessionId,
783
+ userId,
784
+ clientInformation,
785
+ tokens,
786
+ codeVerifier,
787
+ codeVerifierChallenge,
788
+ codeVerifierNonce,
789
+ clientId,
790
+ oauthState
791
+ };
767
792
  }
768
793
  async clearCredentials(userId, sessionId) {
769
794
  await this.patchCredentials(userId, sessionId, {
770
795
  clientInformation: null,
771
796
  tokens: null,
772
797
  codeVerifier: null,
798
+ codeVerifierChallenge: null,
799
+ codeVerifierNonce: null,
773
800
  clientId: null,
774
801
  oauthState: null
775
802
  });
@@ -786,7 +813,6 @@ var FileStorageBackend = class {
786
813
  await this.ensureInitialized();
787
814
  const sessionKey = this.getSessionKey(userId, sessionId);
788
815
  const deleted = this.memoryCache.delete(sessionKey);
789
- this.credentialsCache.delete(sessionKey);
790
816
  if (deleted) {
791
817
  await this.flush();
792
818
  }
@@ -798,7 +824,6 @@ var FileStorageBackend = class {
798
824
  async clearAll() {
799
825
  await this.ensureInitialized();
800
826
  this.memoryCache.clear();
801
- this.credentialsCache.clear();
802
827
  await this.flush();
803
828
  }
804
829
  async cleanupExpired() {
@@ -807,7 +832,6 @@ var FileStorageBackend = class {
807
832
  for (const [key, session] of this.memoryCache.entries()) {
808
833
  if (!isSessionExpired(session)) continue;
809
834
  this.memoryCache.delete(key);
810
- this.credentialsCache.delete(key);
811
835
  changed = true;
812
836
  }
813
837
  if (changed) {
@@ -821,12 +845,10 @@ var SqliteStorage = class {
821
845
  constructor(options = {}) {
822
846
  __publicField(this, "db", null);
823
847
  __publicField(this, "table");
824
- __publicField(this, "credentialsTable");
825
848
  __publicField(this, "initialized", false);
826
849
  __publicField(this, "dbPath");
827
850
  this.dbPath = options.path || "./sessions.db";
828
851
  this.table = options.table || "mcp_sessions";
829
- this.credentialsTable = `${this.table}_credentials`;
830
852
  }
831
853
  async init() {
832
854
  if (this.initialized) return;
@@ -846,13 +868,6 @@ var SqliteStorage = class {
846
868
  expiresAt INTEGER
847
869
  );
848
870
  CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
849
- CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
850
- sessionId TEXT NOT NULL,
851
- userId TEXT NOT NULL,
852
- data TEXT NOT NULL,
853
- PRIMARY KEY (userId, sessionId),
854
- FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
855
- );
856
871
  `);
857
872
  this.initialized = true;
858
873
  console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
@@ -909,41 +924,54 @@ var SqliteStorage = class {
909
924
  }
910
925
  async patchCredentials(userId, sessionId, data) {
911
926
  this.ensureInitialized();
912
- if (!await this.get(userId, sessionId)) {
927
+ const currentSession = await this.get(userId, sessionId, { includeCredentials: true });
928
+ if (!currentSession) {
913
929
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
914
930
  }
915
- const current = await this.getCredentials(userId, sessionId) ?? { sessionId, userId };
916
- const credentials = { ...current, ...data, sessionId, userId };
931
+ const updated = { ...currentSession, ...data, sessionId, userId };
917
932
  const stmt = this.db.prepare(
918
- `INSERT INTO ${this.credentialsTable} (sessionId, userId, data)
919
- VALUES (?, ?, ?)
920
- ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
933
+ `UPDATE ${this.table} SET data = ? WHERE sessionId = ? AND userId = ?`
921
934
  );
922
- stmt.run(sessionId, userId, JSON.stringify(credentials));
935
+ stmt.run(JSON.stringify(updated), sessionId, userId);
923
936
  }
924
- async get(userId, sessionId) {
937
+ async get(userId, sessionId, options) {
925
938
  this.ensureInitialized();
926
939
  const stmt = this.db.prepare(
927
940
  `SELECT data FROM ${this.table} WHERE sessionId = ? AND userId = ?`
928
941
  );
929
942
  const row = stmt.get(sessionId, userId);
930
943
  if (!row) return null;
931
- return normalizeStoredSession(JSON.parse(row.data));
944
+ const session = normalizeStoredSession(JSON.parse(row.data));
945
+ if (!options?.includeCredentials) {
946
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
947
+ return sessionOnly;
948
+ }
949
+ return session;
932
950
  }
933
951
  async getCredentials(userId, sessionId) {
934
952
  this.ensureInitialized();
935
- if (!await this.get(userId, sessionId)) return null;
936
- const stmt = this.db.prepare(
937
- `SELECT data FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
938
- );
939
- const row = stmt.get(sessionId, userId);
940
- return row ? JSON.parse(row.data) : { sessionId, userId };
953
+ const session = await this.get(userId, sessionId, { includeCredentials: true });
954
+ if (!session) return null;
955
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
956
+ return {
957
+ sessionId,
958
+ userId,
959
+ clientInformation,
960
+ tokens,
961
+ codeVerifier,
962
+ codeVerifierChallenge,
963
+ codeVerifierNonce,
964
+ clientId,
965
+ oauthState
966
+ };
941
967
  }
942
968
  async clearCredentials(userId, sessionId) {
943
969
  await this.patchCredentials(userId, sessionId, {
944
970
  clientInformation: null,
945
971
  tokens: null,
946
972
  codeVerifier: null,
973
+ codeVerifierChallenge: null,
974
+ codeVerifierNonce: null,
947
975
  clientId: null,
948
976
  oauthState: null
949
977
  });
@@ -969,9 +997,6 @@ var SqliteStorage = class {
969
997
  const stmt = this.db.prepare(
970
998
  `DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
971
999
  );
972
- this.db.prepare(
973
- `DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
974
- ).run(sessionId, userId);
975
1000
  stmt.run(sessionId, userId);
976
1001
  }
977
1002
  async listAllIds() {
@@ -982,7 +1007,6 @@ var SqliteStorage = class {
982
1007
  }
983
1008
  async clearAll() {
984
1009
  this.ensureInitialized();
985
- this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
986
1010
  const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
987
1011
  stmt.run();
988
1012
  }
@@ -996,14 +1020,6 @@ var SqliteStorage = class {
996
1020
  deleteStmt.run(row.sessionId, row.userId);
997
1021
  }
998
1022
  }
999
- this.db.prepare(
1000
- `DELETE FROM ${this.credentialsTable}
1001
- WHERE NOT EXISTS (
1002
- SELECT 1 FROM ${this.table}
1003
- WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
1004
- AND ${this.table}.userId = ${this.credentialsTable}.userId
1005
- )`
1006
- ).run();
1007
1023
  }
1008
1024
  async disconnect() {
1009
1025
  if (this.db) {
@@ -1085,7 +1101,6 @@ var SupabaseStorageBackend = class {
1085
1101
  }
1086
1102
  async init() {
1087
1103
  await this.assertTable("mcp_sessions", "session_id");
1088
- await this.assertTable("mcp_credentials", "session_id");
1089
1104
  console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
1090
1105
  }
1091
1106
  async assertTable(table, column) {
@@ -1116,18 +1131,13 @@ var SupabaseStorageBackend = class {
1116
1131
  headers: decryptObject(row.headers),
1117
1132
  authUrl: row.auth_url,
1118
1133
  status: row.status ?? "pending",
1119
- toolPolicy: normalizeToolPolicy(row.tool_policy)
1120
- };
1121
- }
1122
- mapRowToCredentials(row, userId, sessionId) {
1123
- return {
1124
- sessionId,
1125
- userId,
1126
- clientInformation: decryptObject(row?.client_information),
1127
- tokens: decryptObject(row?.tokens),
1128
- codeVerifier: decryptObject(row?.code_verifier),
1129
- clientId: row?.client_id,
1130
- oauthState: row?.oauth_state
1134
+ toolPolicy: normalizeToolPolicy(row.tool_policy),
1135
+ clientInformation: decryptObject(row.client_information),
1136
+ tokens: decryptObject(row.tokens),
1137
+ codeVerifier: decryptObject(row.code_verifier),
1138
+ clientId: row.client_id,
1139
+ oauthState: row.oauth_state,
1140
+ enabled: row.enabled ?? true
1131
1141
  };
1132
1142
  }
1133
1143
  hasCredentialData(data) {
@@ -1185,6 +1195,7 @@ var SupabaseStorageBackend = class {
1185
1195
  if ("headers" in data) updateData.headers = encryptObject(data.headers);
1186
1196
  if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
1187
1197
  if ("toolPolicy" in data) updateData.tool_policy = normalizeToolPolicy(data.toolPolicy);
1198
+ if ("enabled" in data) updateData.enabled = data.enabled;
1188
1199
  const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
1189
1200
  let updatedRows = null;
1190
1201
  if (shouldUpdateSession) {
@@ -1206,23 +1217,22 @@ var SupabaseStorageBackend = class {
1206
1217
  }
1207
1218
  async patchCredentials(userId, sessionId, data) {
1208
1219
  if (!this.hasCredentialData(data)) return;
1209
- const row = {
1210
- user_id: userId,
1211
- session_id: sessionId,
1220
+ const updateData = {
1212
1221
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
1213
1222
  };
1214
- if ("clientInformation" in data) row.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
1215
- if ("tokens" in data) row.tokens = data.tokens == null ? null : encryptObject(data.tokens);
1216
- if ("codeVerifier" in data) row.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
1217
- if ("clientId" in data) row.client_id = data.clientId ?? null;
1218
- if ("oauthState" in data) row.oauth_state = data.oauthState ?? null;
1219
- const { error } = await this.supabase.from("mcp_credentials").upsert(row, { onConflict: "user_id,session_id" });
1223
+ if ("clientInformation" in data) updateData.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
1224
+ if ("tokens" in data) updateData.tokens = data.tokens == null ? null : encryptObject(data.tokens);
1225
+ if ("codeVerifier" in data) updateData.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
1226
+ if ("clientId" in data) updateData.client_id = data.clientId ?? null;
1227
+ if ("oauthState" in data) updateData.oauth_state = data.oauthState ?? null;
1228
+ const { error } = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId);
1220
1229
  if (error) {
1221
1230
  throw new Error(`Failed to update credentials: ${error.message}`);
1222
1231
  }
1223
1232
  }
1224
- async get(userId, sessionId) {
1225
- const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
1233
+ async get(userId, sessionId, options) {
1234
+ const selection = options?.includeCredentials ? "*" : "session_id, user_id, server_id, server_name, server_url, transport_type, callback_url, created_at, updated_at, expires_at, headers, auth_url, status, tool_policy, enabled";
1235
+ const { data, error } = await this.supabase.from("mcp_sessions").select(selection).eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
1226
1236
  if (error) {
1227
1237
  console.error("[SupabaseStorage] Failed to get session:", error);
1228
1238
  return null;
@@ -1231,19 +1241,21 @@ var SupabaseStorageBackend = class {
1231
1241
  return this.mapRowToSessionData(data);
1232
1242
  }
1233
1243
  async getCredentials(userId, sessionId) {
1234
- const { data, error } = await this.supabase.from("mcp_credentials").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
1244
+ const { data, error } = await this.supabase.from("mcp_sessions").select("client_information, tokens, code_verifier, client_id, oauth_state").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
1235
1245
  if (error) {
1236
1246
  console.error("[SupabaseStorage] Failed to get credentials:", error);
1237
1247
  return null;
1238
1248
  }
1239
- if (data) {
1240
- return this.mapRowToCredentials(data, userId, sessionId);
1241
- }
1242
- const { data: sessionRows, error: sessionError } = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
1243
- if (sessionError || !sessionRows || sessionRows.length === 0) {
1244
- return null;
1245
- }
1246
- return { sessionId, userId };
1249
+ if (!data) return null;
1250
+ return {
1251
+ sessionId,
1252
+ userId,
1253
+ clientInformation: decryptObject(data.client_information),
1254
+ tokens: decryptObject(data.tokens),
1255
+ codeVerifier: decryptObject(data.code_verifier),
1256
+ clientId: data.client_id,
1257
+ oauthState: data.oauth_state
1258
+ };
1247
1259
  }
1248
1260
  async list(userId) {
1249
1261
  const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
@@ -1254,7 +1266,14 @@ var SupabaseStorageBackend = class {
1254
1266
  return data.map((row) => this.mapRowToSessionData(row));
1255
1267
  }
1256
1268
  async clearCredentials(userId, sessionId) {
1257
- const { error } = await this.supabase.from("mcp_credentials").delete().eq("user_id", userId).eq("session_id", sessionId);
1269
+ const { error } = await this.supabase.from("mcp_sessions").update({
1270
+ client_information: null,
1271
+ tokens: null,
1272
+ code_verifier: null,
1273
+ client_id: null,
1274
+ oauth_state: null,
1275
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1276
+ }).eq("user_id", userId).eq("session_id", sessionId);
1258
1277
  if (error) {
1259
1278
  throw new Error(`Failed to clear credentials: ${error.message}`);
1260
1279
  }
@@ -1282,10 +1301,6 @@ var SupabaseStorageBackend = class {
1282
1301
  return data.map((row) => row.session_id);
1283
1302
  }
1284
1303
  async clearAll() {
1285
- const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
1286
- if (credentialsError) {
1287
- console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
1288
- }
1289
1304
  const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
1290
1305
  if (error) {
1291
1306
  console.error("[SupabaseStorage] Failed to clear sessions:", error);
@@ -1311,16 +1326,12 @@ var NeonStorageBackend = class {
1311
1326
  constructor(sql, options = {}) {
1312
1327
  this.sql = sql;
1313
1328
  __publicField(this, "tableName");
1314
- __publicField(this, "credentialsTableName");
1315
1329
  const schema = options.schema || "public";
1316
1330
  const table = options.table || "mcp_sessions";
1317
- const credentialsTable = options.credentialsTable || "mcp_credentials";
1318
1331
  this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
1319
- this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
1320
1332
  }
1321
1333
  async init() {
1322
1334
  await this.assertTable(this.tableName, "mcp_sessions");
1323
- await this.assertTable(this.credentialsTableName, "mcp_credentials");
1324
1335
  console.log("[mcp-ts][Storage] Neon: storage tables verified.");
1325
1336
  }
1326
1337
  async assertTable(qualifiedName, displayName) {
@@ -1358,18 +1369,13 @@ var NeonStorageBackend = class {
1358
1369
  headers: decryptObject(row.headers),
1359
1370
  authUrl: row.auth_url ?? void 0,
1360
1371
  status: row.status ?? "pending",
1361
- toolPolicy: normalizeToolPolicy(row.tool_policy)
1362
- };
1363
- }
1364
- mapRowToCredentials(row, userId, sessionId) {
1365
- return {
1366
- sessionId,
1367
- userId,
1372
+ toolPolicy: normalizeToolPolicy(row.tool_policy),
1368
1373
  clientInformation: decryptObject(row.client_information),
1369
1374
  tokens: decryptObject(row.tokens),
1370
1375
  codeVerifier: decryptObject(row.code_verifier),
1371
1376
  clientId: row.client_id ?? void 0,
1372
- oauthState: row.oauth_state
1377
+ oauthState: row.oauth_state,
1378
+ enabled: row.enabled ?? true
1373
1379
  };
1374
1380
  }
1375
1381
  hasCredentialData(data) {
@@ -1439,7 +1445,7 @@ var NeonStorageBackend = class {
1439
1445
  const updatedSession = { ...currentSession, ...data };
1440
1446
  const status = updatedSession.status ?? "pending";
1441
1447
  const expiresAt = resolveSessionExpiresAt(status);
1442
- 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 || "toolPolicy" in data;
1448
+ 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 || "toolPolicy" in data || "enabled" in data;
1443
1449
  if (shouldUpdateSession) {
1444
1450
  const setClauses = [];
1445
1451
  const values = [];
@@ -1462,6 +1468,9 @@ var NeonStorageBackend = class {
1462
1468
  const toolPolicy = normalizeToolPolicy(updatedSession.toolPolicy, policyUpdatedAt) ?? { mode: "all", toolIds: [], updatedAt: policyUpdatedAt };
1463
1469
  addSet("tool_policy", toolPolicy);
1464
1470
  }
1471
+ if ("enabled" in data) {
1472
+ addSet("enabled", updatedSession.enabled);
1473
+ }
1465
1474
  setClauses.push("updated_at = now()");
1466
1475
  const updatedRows = await this.sql.query(
1467
1476
  `UPDATE ${this.tableName}
@@ -1477,45 +1486,45 @@ var NeonStorageBackend = class {
1477
1486
  }
1478
1487
  async patchCredentials(userId, sessionId, data) {
1479
1488
  if (!this.hasCredentialData(data)) return;
1480
- await this.sql.query(
1481
- `INSERT INTO ${this.credentialsTableName} (
1482
- user_id,
1483
- session_id,
1484
- client_information,
1485
- tokens,
1486
- code_verifier,
1487
- client_id,
1488
- oauth_state,
1489
- updated_at
1490
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, now())
1491
- ON CONFLICT (user_id, session_id)
1492
- DO UPDATE SET
1493
- client_information = CASE WHEN $8 THEN EXCLUDED.client_information ELSE ${this.credentialsTableName}.client_information END,
1494
- tokens = CASE WHEN $9 THEN EXCLUDED.tokens ELSE ${this.credentialsTableName}.tokens END,
1495
- code_verifier = CASE WHEN $10 THEN EXCLUDED.code_verifier ELSE ${this.credentialsTableName}.code_verifier END,
1496
- client_id = CASE WHEN $11 THEN EXCLUDED.client_id ELSE ${this.credentialsTableName}.client_id END,
1497
- oauth_state = CASE WHEN $12 THEN EXCLUDED.oauth_state ELSE ${this.credentialsTableName}.oauth_state END,
1498
- updated_at = now()`,
1499
- [
1500
- userId,
1501
- sessionId,
1502
- "clientInformation" in data ? data.clientInformation == null ? null : encryptObject(data.clientInformation) : null,
1503
- "tokens" in data ? data.tokens == null ? null : encryptObject(data.tokens) : null,
1504
- "codeVerifier" in data ? data.codeVerifier == null ? null : encryptObject(data.codeVerifier) : null,
1505
- "clientId" in data ? data.clientId ?? null : null,
1506
- "oauthState" in data ? data.oauthState ?? null : null,
1507
- "clientInformation" in data,
1508
- "tokens" in data,
1509
- "codeVerifier" in data,
1510
- "clientId" in data,
1511
- "oauthState" in data
1512
- ]
1489
+ const setClauses = [];
1490
+ const values = [];
1491
+ let paramIndex = 1;
1492
+ const addSet = (column, value) => {
1493
+ setClauses.push(`${column} = $${paramIndex++}`);
1494
+ values.push(value);
1495
+ };
1496
+ if ("clientInformation" in data) {
1497
+ addSet("client_information", data.clientInformation == null ? null : encryptObject(data.clientInformation));
1498
+ }
1499
+ if ("tokens" in data) {
1500
+ addSet("tokens", data.tokens == null ? null : encryptObject(data.tokens));
1501
+ }
1502
+ if ("codeVerifier" in data) {
1503
+ addSet("code_verifier", data.codeVerifier == null ? null : encryptObject(data.codeVerifier));
1504
+ }
1505
+ if ("clientId" in data) {
1506
+ addSet("client_id", data.clientId ?? null);
1507
+ }
1508
+ if ("oauthState" in data) {
1509
+ addSet("oauth_state", data.oauthState ?? null);
1510
+ }
1511
+ setClauses.push("updated_at = now()");
1512
+ const updatedRows = await this.sql.query(
1513
+ `UPDATE ${this.tableName}
1514
+ SET ${setClauses.join(", ")}
1515
+ WHERE user_id = $${paramIndex++} AND session_id = $${paramIndex++}
1516
+ RETURNING id`,
1517
+ [...values, userId, sessionId]
1513
1518
  );
1519
+ if (updatedRows.length === 0) {
1520
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
1521
+ }
1514
1522
  }
1515
- async get(userId, sessionId) {
1523
+ async get(userId, sessionId, options) {
1516
1524
  try {
1525
+ const selection = options?.includeCredentials ? "*" : "session_id, user_id, server_id, server_name, server_url, transport_type, callback_url, created_at, updated_at, expires_at, headers, auth_url, status, tool_policy, enabled";
1517
1526
  const rows = await this.sql.query(
1518
- `SELECT * FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1527
+ `SELECT ${selection} FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1519
1528
  [userId, sessionId]
1520
1529
  );
1521
1530
  if (!rows[0]) return null;
@@ -1527,18 +1536,22 @@ var NeonStorageBackend = class {
1527
1536
  }
1528
1537
  async getCredentials(userId, sessionId) {
1529
1538
  try {
1530
- const credentialRows = await this.sql.query(
1531
- `SELECT * FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
1532
- [userId, sessionId]
1533
- );
1534
- if (credentialRows[0]) {
1535
- return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
1536
- }
1537
- const sessionRows = await this.sql.query(
1538
- `SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1539
+ const rows = await this.sql.query(
1540
+ `SELECT client_information, tokens, code_verifier, client_id, oauth_state
1541
+ FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1539
1542
  [userId, sessionId]
1540
1543
  );
1541
- return sessionRows[0] ? { sessionId, userId } : null;
1544
+ if (!rows[0]) return null;
1545
+ const row = rows[0];
1546
+ return {
1547
+ sessionId,
1548
+ userId,
1549
+ clientInformation: decryptObject(row.client_information),
1550
+ tokens: decryptObject(row.tokens),
1551
+ codeVerifier: decryptObject(row.code_verifier),
1552
+ clientId: row.client_id ?? void 0,
1553
+ oauthState: row.oauth_state
1554
+ };
1542
1555
  } catch (error) {
1543
1556
  console.error("[NeonStorage] Failed to get credentials:", error);
1544
1557
  return null;
@@ -1559,7 +1572,9 @@ var NeonStorageBackend = class {
1559
1572
  async clearCredentials(userId, sessionId) {
1560
1573
  try {
1561
1574
  await this.sql.query(
1562
- `DELETE FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
1575
+ `UPDATE ${this.tableName}
1576
+ SET client_information = null, tokens = null, code_verifier = null, client_id = null, oauth_state = null, updated_at = now()
1577
+ WHERE user_id = $1 AND session_id = $2`,
1563
1578
  [userId, sessionId]
1564
1579
  );
1565
1580
  } catch (error) {
@@ -1601,7 +1616,6 @@ var NeonStorageBackend = class {
1601
1616
  }
1602
1617
  async clearAll() {
1603
1618
  try {
1604
- await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
1605
1619
  await this.sql.query(`DELETE FROM ${this.tableName}`);
1606
1620
  } catch (error) {
1607
1621
  console.error("[NeonStorage] Failed to clear sessions:", error);
@@ -1843,6 +1857,60 @@ function onSessionMutation(listener) {
1843
1857
  sessionMutationListeners.delete(listener);
1844
1858
  };
1845
1859
  }
1860
+ function withDbObservability(store, emit) {
1861
+ const readMethods = /* @__PURE__ */ new Set([
1862
+ "get",
1863
+ "getCredentials",
1864
+ "list",
1865
+ "listIds",
1866
+ "listAllIds"
1867
+ ]);
1868
+ return new Proxy(store, {
1869
+ get(target, prop, receiver) {
1870
+ const original = Reflect.get(target, prop, receiver);
1871
+ if (typeof original !== "function") return original;
1872
+ return (...args) => {
1873
+ const method = prop;
1874
+ const isRead = readMethods.has(prop);
1875
+ const start = performance.now();
1876
+ const emitEvent = (error) => {
1877
+ emit({
1878
+ type: isRead ? "db:read" : "db:write",
1879
+ level: error ? "error" : "debug",
1880
+ message: `${method}(${args.map(
1881
+ (a) => typeof a === "string" ? a.slice(0, 24) : typeof a
1882
+ ).join(", ")})`,
1883
+ sessionId: typeof args[1] === "string" ? args[1] : void 0,
1884
+ payload: {
1885
+ method,
1886
+ argTypes: args.map((a) => typeof a),
1887
+ durationMs: performance.now() - start,
1888
+ ...error ? { error } : {}
1889
+ },
1890
+ timestamp: Date.now()
1891
+ });
1892
+ };
1893
+ try {
1894
+ const result = original.apply(target, args);
1895
+ if (result instanceof Promise) {
1896
+ return result.then((r) => {
1897
+ emitEvent();
1898
+ return r;
1899
+ }).catch((e) => {
1900
+ emitEvent(String(e));
1901
+ throw e;
1902
+ });
1903
+ }
1904
+ emitEvent();
1905
+ return result;
1906
+ } catch (e) {
1907
+ emitEvent(String(e));
1908
+ throw e;
1909
+ }
1910
+ };
1911
+ }
1912
+ });
1913
+ }
1846
1914
  var sessions = new Proxy({}, {
1847
1915
  get(_target, prop) {
1848
1916
  return async (...args) => {
@@ -1862,6 +1930,24 @@ var sessions = new Proxy({}, {
1862
1930
  });
1863
1931
 
1864
1932
  // src/server/mcp/storage-oauth-provider.ts
1933
+ var codeVerifierContext = new AsyncLocalStorage();
1934
+ function runWithCodeVerifierState(verifier, method, fn) {
1935
+ return codeVerifierContext.run({ verifier, method }, fn);
1936
+ }
1937
+ function base64UrlEncode(bytes) {
1938
+ let binary = "";
1939
+ for (const byte of bytes) {
1940
+ binary += String.fromCharCode(byte);
1941
+ }
1942
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1943
+ }
1944
+ async function createCodeChallenge(verifier) {
1945
+ const digest = await crypto.subtle.digest(
1946
+ "SHA-256",
1947
+ new TextEncoder().encode(verifier)
1948
+ );
1949
+ return base64UrlEncode(new Uint8Array(digest));
1950
+ }
1865
1951
  var StorageOAuthClientProvider = class {
1866
1952
  /**
1867
1953
  * Creates a new session-backed OAuth provider
@@ -1877,8 +1963,12 @@ var StorageOAuthClientProvider = class {
1877
1963
  __publicField(this, "logoUri");
1878
1964
  __publicField(this, "policyUri");
1879
1965
  __publicField(this, "clientSecret");
1966
+ __publicField(this, "_store");
1880
1967
  __publicField(this, "_authUrl");
1881
1968
  __publicField(this, "_clientId");
1969
+ __publicField(this, "_cachedCodeVerifier");
1970
+ __publicField(this, "_hasCodeVerifier", false);
1971
+ __publicField(this, "_cachedTokens");
1882
1972
  __publicField(this, "onRedirectCallback");
1883
1973
  this.userId = options.userId;
1884
1974
  this.serverId = options.serverId;
@@ -1890,6 +1980,8 @@ var StorageOAuthClientProvider = class {
1890
1980
  this.policyUri = options.policyUri;
1891
1981
  this._clientId = options.clientId;
1892
1982
  this.clientSecret = options.clientSecret;
1983
+ this._cachedTokens = options.cachedTokens;
1984
+ this._store = options.sessionStore ?? sessions;
1893
1985
  this.onRedirectCallback = options.onRedirect;
1894
1986
  }
1895
1987
  get clientMetadata() {
@@ -1917,7 +2009,7 @@ var StorageOAuthClientProvider = class {
1917
2009
  * @private
1918
2010
  */
1919
2011
  async getCredentials() {
1920
- const data = await sessions.getCredentials(this.userId, this.sessionId);
2012
+ const data = await this._store.getCredentials(this.userId, this.sessionId);
1921
2013
  if (!data) {
1922
2014
  return { userId: this.userId, sessionId: this.sessionId };
1923
2015
  }
@@ -1930,35 +2022,33 @@ var StorageOAuthClientProvider = class {
1930
2022
  * @throws Error if session doesn't exist (session must be created by controller layer)
1931
2023
  */
1932
2024
  async patchCredentials(data) {
1933
- await sessions.patchCredentials(this.userId, this.sessionId, data);
2025
+ await this._store.patchCredentials(this.userId, this.sessionId, data);
1934
2026
  }
1935
2027
  /**
1936
- * Retrieves stored OAuth client information
2028
+ * Retrieves stored OAuth client information.
1937
2029
  */
1938
2030
  async clientInformation() {
1939
- const data = await this.getCredentials();
1940
- if (data.clientId && !this._clientId) {
1941
- this._clientId = data.clientId;
1942
- }
1943
- if (this._clientId && !data.clientId) {
1944
- await this.patchCredentials({
1945
- clientId: this._clientId,
1946
- clientInformation: {
1947
- client_id: this._clientId,
1948
- ...this.clientSecret ? { client_secret: this.clientSecret } : {}
1949
- }
1950
- });
2031
+ if (this._clientId) {
2032
+ return {
2033
+ client_id: this._clientId,
2034
+ ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2035
+ };
1951
2036
  }
1952
- if (data.clientInformation) {
1953
- return data.clientInformation;
2037
+ const data = await this.getCredentials();
2038
+ if (this._cachedTokens === void 0) {
2039
+ this._cachedTokens = data.tokens ?? null;
1954
2040
  }
1955
- if (!this._clientId) {
1956
- return void 0;
2041
+ if (data.clientId) {
2042
+ this._clientId = data.clientId;
2043
+ if (data.clientInformation) {
2044
+ return data.clientInformation;
2045
+ }
2046
+ return {
2047
+ client_id: data.clientId,
2048
+ ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2049
+ };
1957
2050
  }
1958
- return {
1959
- client_id: this._clientId,
1960
- ...this.clientSecret ? { client_secret: this.clientSecret } : {}
1961
- };
2051
+ return void 0;
1962
2052
  }
1963
2053
  /**
1964
2054
  * Stores OAuth client information
@@ -1975,11 +2065,30 @@ var StorageOAuthClientProvider = class {
1975
2065
  */
1976
2066
  async saveTokens(tokens) {
1977
2067
  await this.patchCredentials({ tokens });
2068
+ this._cachedTokens = tokens;
2069
+ }
2070
+ /**
2071
+ * Persists static client credentials to DB immediately on creation.
2072
+ * Ensures getOrCreateClient() finds them on rehydration, even when the
2073
+ * server allows anonymous connect and never triggers DCR.
2074
+ */
2075
+ async initializeCredentials() {
2076
+ if (this._clientId) {
2077
+ await this.patchCredentials({
2078
+ clientId: this._clientId,
2079
+ clientInformation: {
2080
+ client_id: this._clientId,
2081
+ ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2082
+ }
2083
+ });
2084
+ }
1978
2085
  }
1979
2086
  get authUrl() {
1980
2087
  return this._authUrl;
1981
2088
  }
1982
2089
  async state() {
2090
+ this._cachedCodeVerifier = void 0;
2091
+ this._hasCodeVerifier = false;
1983
2092
  const nonce = nanoid(32);
1984
2093
  await this.patchCredentials({
1985
2094
  oauthState: {
@@ -2000,7 +2109,7 @@ var StorageOAuthClientProvider = class {
2000
2109
  if (parsed.sessionId !== this.sessionId) {
2001
2110
  return { valid: false, error: "OAuth state mismatch" };
2002
2111
  }
2003
- const data = await sessions.getCredentials(this.userId, parsed.sessionId);
2112
+ const data = await this._store.getCredentials(this.userId, parsed.sessionId);
2004
2113
  if (!data) {
2005
2114
  return { valid: false, error: "Session not found" };
2006
2115
  }
@@ -2025,49 +2134,77 @@ var StorageOAuthClientProvider = class {
2025
2134
  }
2026
2135
  async redirectToAuthorization(authUrl) {
2027
2136
  this._authUrl = authUrl.toString();
2028
- await sessions.update(this.userId, this.sessionId, { authUrl: this._authUrl });
2137
+ const codeChallenge = authUrl.searchParams.get("code_challenge");
2138
+ const state = authUrl.searchParams.get("state");
2139
+ if (this._cachedCodeVerifier && codeChallenge && state) {
2140
+ const expectedChallenge = await createCodeChallenge(this._cachedCodeVerifier);
2141
+ if (expectedChallenge === codeChallenge) {
2142
+ const parsed = parseOAuthState(state);
2143
+ if (parsed) {
2144
+ await this.patchCredentials({
2145
+ codeVerifier: this._cachedCodeVerifier
2146
+ });
2147
+ }
2148
+ }
2149
+ }
2029
2150
  if (this.onRedirectCallback) {
2030
2151
  this.onRedirectCallback(authUrl.toString());
2031
2152
  }
2032
2153
  }
2033
2154
  async invalidateCredentials(scope) {
2034
2155
  if (scope === "all") {
2035
- await sessions.delete(this.userId, this.sessionId);
2156
+ this._cachedTokens = void 0;
2157
+ await this._store.delete(this.userId, this.sessionId);
2036
2158
  } else {
2037
2159
  const updates = {};
2038
2160
  if (scope === "client") {
2039
2161
  updates.clientInformation = null;
2040
2162
  updates.clientId = null;
2041
2163
  } else if (scope === "tokens") {
2164
+ this._cachedTokens = void 0;
2042
2165
  updates.tokens = null;
2043
2166
  } else if (scope === "verifier") {
2167
+ this._cachedCodeVerifier = void 0;
2168
+ this._hasCodeVerifier = false;
2044
2169
  updates.codeVerifier = null;
2045
2170
  }
2046
2171
  await this.patchCredentials(updates);
2047
2172
  }
2048
2173
  }
2049
2174
  async saveCodeVerifier(verifier) {
2050
- const data = await this.getCredentials();
2051
- if (data.codeVerifier) {
2175
+ if (this._hasCodeVerifier) {
2052
2176
  return;
2053
2177
  }
2054
- await this.patchCredentials({ codeVerifier: verifier });
2178
+ this._cachedCodeVerifier = verifier;
2179
+ this._hasCodeVerifier = true;
2055
2180
  }
2056
2181
  async codeVerifier() {
2057
- const data = await this.getCredentials();
2058
- if (data.clientId && !this._clientId) {
2059
- this._clientId = data.clientId;
2182
+ if (this._cachedCodeVerifier) {
2183
+ return this._cachedCodeVerifier;
2060
2184
  }
2061
- if (!data.codeVerifier) {
2062
- throw new Error("No code verifier found");
2185
+ const ctx = codeVerifierContext.getStore();
2186
+ if (ctx?.verifier) {
2187
+ return ctx.verifier;
2188
+ }
2189
+ const data = await this.getCredentials();
2190
+ if (data.codeVerifier) {
2191
+ return data.codeVerifier;
2063
2192
  }
2064
- return data.codeVerifier;
2193
+ throw new Error("No code verifier found");
2065
2194
  }
2066
2195
  async deleteCodeVerifier() {
2067
- await this.patchCredentials({ codeVerifier: null });
2196
+ this._cachedCodeVerifier = void 0;
2197
+ this._hasCodeVerifier = false;
2198
+ await this.patchCredentials({
2199
+ codeVerifier: null
2200
+ });
2068
2201
  }
2069
2202
  async tokens() {
2203
+ if (this._cachedTokens !== void 0) {
2204
+ return this._cachedTokens ?? void 0;
2205
+ }
2070
2206
  const data = await this.getCredentials();
2207
+ this._cachedTokens = data.tokens ?? null;
2071
2208
  if (data.clientId && !this._clientId) {
2072
2209
  this._clientId = data.clientId;
2073
2210
  }
@@ -2160,23 +2297,10 @@ var MCPClient = class {
2160
2297
  __publicField(this, "client");
2161
2298
  __publicField(this, "oauthProvider", null);
2162
2299
  __publicField(this, "transport", null);
2163
- __publicField(this, "userId");
2164
- __publicField(this, "serverId");
2165
- __publicField(this, "sessionId");
2166
- __publicField(this, "serverName");
2167
- __publicField(this, "transportType");
2168
- __publicField(this, "serverUrl");
2169
- __publicField(this, "callbackUrl");
2170
- __publicField(this, "onRedirect");
2171
- __publicField(this, "clientId");
2172
- __publicField(this, "clientSecret");
2173
- __publicField(this, "headers");
2174
- /** OAuth Client Metadata */
2175
- __publicField(this, "clientName");
2176
- __publicField(this, "clientUri");
2177
- __publicField(this, "logoUri");
2178
- __publicField(this, "policyUri");
2300
+ __publicField(this, "config");
2179
2301
  __publicField(this, "createdAt");
2302
+ __publicField(this, "_serverInfo");
2303
+ __publicField(this, "_store");
2180
2304
  /** Event emitters for connection lifecycle */
2181
2305
  __publicField(this, "_onConnectionEvent", new Emitter());
2182
2306
  __publicField(this, "onConnectionEvent", this._onConnectionEvent.event);
@@ -2192,21 +2316,8 @@ var MCPClient = class {
2192
2316
  * the memory when the client is no longer needed.
2193
2317
  */
2194
2318
  __publicField(this, "cachedTools", null);
2195
- this.serverUrl = options.serverUrl;
2196
- this.serverName = options.serverName;
2197
- this.callbackUrl = options.callbackUrl;
2198
- this.onRedirect = options.onRedirect;
2199
- this.userId = options.userId;
2200
- this.serverId = options.serverId;
2201
- this.sessionId = options.sessionId;
2202
- this.transportType = options.transportType;
2203
- this.clientId = options.clientId;
2204
- this.clientSecret = options.clientSecret;
2205
- this.headers = options.headers;
2206
- this.clientName = options.clientName;
2207
- this.clientUri = options.clientUri;
2208
- this.logoUri = options.logoUri;
2209
- this.policyUri = options.policyUri;
2319
+ this.config = { ...options };
2320
+ this._store = options.sessionStore ?? sessions;
2210
2321
  this.client = new Client(
2211
2322
  {
2212
2323
  name: MCP_CLIENT_NAME,
@@ -2223,6 +2334,21 @@ var MCPClient = class {
2223
2334
  }
2224
2335
  );
2225
2336
  }
2337
+ /** Shared session-shaped data for ensureSession and saveSession */
2338
+ get session() {
2339
+ return {
2340
+ sessionId: this.config.sessionId,
2341
+ userId: this.config.userId,
2342
+ serverId: this.config.serverId,
2343
+ serverName: this.config.serverName,
2344
+ serverUrl: this.config.serverUrl,
2345
+ callbackUrl: this.config.callbackUrl,
2346
+ transportType: this.config.transportType || "streamable-http",
2347
+ headers: this.config.headers,
2348
+ createdAt: this.createdAt ?? Date.now(),
2349
+ updatedAt: Date.now()
2350
+ };
2351
+ }
2226
2352
  /**
2227
2353
  * Emit a connection state change event
2228
2354
  * @private
@@ -2230,13 +2356,13 @@ var MCPClient = class {
2230
2356
  emitStateChange(newState) {
2231
2357
  const previousState = this.currentState;
2232
2358
  this.currentState = newState;
2233
- if (!this.serverId) return;
2359
+ if (!this.config.serverId) return;
2234
2360
  this._onConnectionEvent.fire({
2235
2361
  type: "state_changed",
2236
- sessionId: this.sessionId,
2237
- serverId: this.serverId,
2238
- serverName: this.serverName || this.serverId,
2239
- serverUrl: this.serverUrl || "",
2362
+ sessionId: this.config.sessionId,
2363
+ serverId: this.config.serverId,
2364
+ serverName: this.config.serverName || this.config.serverId,
2365
+ serverUrl: this.config.serverUrl || "",
2240
2366
  createdAt: this.createdAt,
2241
2367
  state: newState,
2242
2368
  previousState,
@@ -2247,8 +2373,8 @@ var MCPClient = class {
2247
2373
  level: "info",
2248
2374
  message: `Connection state: ${previousState} \u2192 ${newState}`,
2249
2375
  displayMessage: `State changed to ${newState}`,
2250
- sessionId: this.sessionId,
2251
- serverId: this.serverId,
2376
+ sessionId: this.config.sessionId,
2377
+ serverId: this.config.serverId,
2252
2378
  payload: { previousState, newState },
2253
2379
  timestamp: Date.now(),
2254
2380
  id: nanoid()
@@ -2259,11 +2385,11 @@ var MCPClient = class {
2259
2385
  * @private
2260
2386
  */
2261
2387
  emitError(error, errorType = "unknown") {
2262
- if (!this.serverId) return;
2388
+ if (!this.config.serverId) return;
2263
2389
  this._onConnectionEvent.fire({
2264
2390
  type: "error",
2265
- sessionId: this.sessionId,
2266
- serverId: this.serverId,
2391
+ sessionId: this.config.sessionId,
2392
+ serverId: this.config.serverId,
2267
2393
  error,
2268
2394
  errorType,
2269
2395
  timestamp: Date.now()
@@ -2273,8 +2399,8 @@ var MCPClient = class {
2273
2399
  level: "error",
2274
2400
  message: error,
2275
2401
  displayMessage: error,
2276
- sessionId: this.sessionId,
2277
- serverId: this.serverId,
2402
+ sessionId: this.config.sessionId,
2403
+ serverId: this.config.serverId,
2278
2404
  payload: { errorType, error },
2279
2405
  timestamp: Date.now(),
2280
2406
  id: nanoid()
@@ -2285,11 +2411,11 @@ var MCPClient = class {
2285
2411
  * @private
2286
2412
  */
2287
2413
  emitProgress(message) {
2288
- if (!this.serverId) return;
2414
+ if (!this.config.serverId) return;
2289
2415
  this._onConnectionEvent.fire({
2290
2416
  type: "progress",
2291
- sessionId: this.sessionId,
2292
- serverId: this.serverId,
2417
+ sessionId: this.config.sessionId,
2418
+ serverId: this.config.serverId,
2293
2419
  message,
2294
2420
  timestamp: Date.now()
2295
2421
  });
@@ -2307,14 +2433,14 @@ var MCPClient = class {
2307
2433
  * @private
2308
2434
  */
2309
2435
  getTransport(type) {
2310
- if (!this.serverUrl) {
2436
+ if (!this.config.serverUrl) {
2311
2437
  throw new Error("Server URL is required to create transport");
2312
2438
  }
2313
- const baseUrl = new URL(this.serverUrl);
2314
- const hasAuthorizationHeader = Object.keys(this.headers || {}).some((key) => key.toLowerCase() === "authorization");
2439
+ const baseUrl = new URL(this.config.serverUrl);
2440
+ const hasAuthorizationHeader = Object.keys(this.config.headers || {}).some((key) => key.toLowerCase() === "authorization");
2315
2441
  const transportOptions = {
2316
2442
  ...!hasAuthorizationHeader && { authProvider: this.oauthProvider },
2317
- ...this.headers && { requestInit: { headers: this.headers } },
2443
+ ...this.config.headers && { requestInit: { headers: this.config.headers } },
2318
2444
  /**
2319
2445
  * Custom fetch implementation to handle connection timeouts.
2320
2446
  * Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
@@ -2352,78 +2478,74 @@ var MCPClient = class {
2352
2478
  * @private
2353
2479
  */
2354
2480
  async ensureSession() {
2355
- if (this.oauthProvider) {
2356
- return;
2357
- }
2481
+ if (this.oauthProvider) return;
2358
2482
  this.emitStateChange("INITIALIZING");
2359
2483
  this.emitProgress("Loading session configuration...");
2360
- let existingSession = null;
2361
- if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
2362
- existingSession = await sessions.get(this.userId, this.sessionId);
2363
- if (!existingSession) {
2364
- throw new Error(`Session not found: ${this.sessionId}`);
2484
+ if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
2485
+ const existingSession2 = await this._store.get(this.config.userId, this.config.sessionId);
2486
+ if (!existingSession2) {
2487
+ throw new Error(`Session not found: ${this.config.sessionId}`);
2365
2488
  }
2366
- this.serverUrl = this.serverUrl || existingSession.serverUrl;
2367
- this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
2368
- this.serverName = this.serverName || existingSession.serverName;
2369
- this.serverId = this.serverId || existingSession.serverId || "unknown";
2370
- this.headers = this.headers || existingSession.headers;
2371
- this.createdAt = existingSession.createdAt;
2372
- }
2373
- if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
2489
+ this.config.serverUrl = this.config.serverUrl || existingSession2.serverUrl;
2490
+ this.config.callbackUrl = this.config.callbackUrl || existingSession2.callbackUrl;
2491
+ this.config.serverName = this.config.serverName || existingSession2.serverName;
2492
+ this.config.serverId = this.config.serverId || existingSession2.serverId || "unknown";
2493
+ this.config.headers = this.config.headers || existingSession2.headers;
2494
+ this.createdAt = existingSession2.createdAt;
2495
+ }
2496
+ if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
2374
2497
  throw new Error("Missing required connection metadata");
2375
2498
  }
2376
- if (!this.oauthProvider) {
2377
- if (!this.serverId) {
2378
- throw new Error("serverId required for OAuth provider initialization");
2379
- }
2380
- this.oauthProvider = new StorageOAuthClientProvider({
2381
- userId: this.userId,
2382
- serverId: this.serverId,
2383
- sessionId: this.sessionId,
2384
- redirectUrl: this.callbackUrl,
2385
- clientName: this.clientName,
2386
- clientUri: this.clientUri,
2387
- logoUri: this.logoUri,
2388
- policyUri: this.policyUri,
2389
- clientId: this.clientId,
2390
- clientSecret: this.clientSecret,
2391
- onRedirect: (redirectUrl) => {
2392
- if (this.onRedirect) {
2393
- this.onRedirect(redirectUrl);
2394
- }
2499
+ this.oauthProvider = new StorageOAuthClientProvider({
2500
+ userId: this.config.userId,
2501
+ serverId: this.config.serverId,
2502
+ sessionId: this.config.sessionId,
2503
+ redirectUrl: this.config.callbackUrl,
2504
+ clientName: this.config.clientName,
2505
+ clientUri: this.config.clientUri,
2506
+ logoUri: this.config.logoUri,
2507
+ policyUri: this.config.policyUri,
2508
+ clientId: this.config.clientId,
2509
+ clientSecret: this.config.clientSecret,
2510
+ cachedTokens: this.config.cachedCredentials?.tokens,
2511
+ sessionStore: this._store,
2512
+ onRedirect: (redirectUrl) => {
2513
+ if (this.config.serverId) {
2514
+ this._onConnectionEvent.fire({
2515
+ type: "auth_required",
2516
+ sessionId: this.config.sessionId,
2517
+ serverId: this.config.serverId,
2518
+ authUrl: redirectUrl,
2519
+ timestamp: Date.now()
2520
+ });
2395
2521
  }
2396
- });
2397
- }
2398
- if (existingSession === null) {
2399
- existingSession = await sessions.get(this.userId, this.sessionId);
2400
- }
2401
- if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
2522
+ if (this.config.onRedirect) {
2523
+ this.config.onRedirect(redirectUrl);
2524
+ }
2525
+ }
2526
+ });
2527
+ const existingSession = this.config.hasSession ? {} : await this._store.get(this.config.userId, this.config.sessionId);
2528
+ if (!existingSession) {
2402
2529
  this.createdAt = Date.now();
2403
2530
  const updatedAt = this.createdAt;
2404
2531
  this._onObservabilityEvent.fire({
2405
2532
  type: "mcp:client:session_created",
2406
2533
  level: "info",
2407
- message: `Creating pending session ${this.sessionId} for connection setup`,
2408
- sessionId: this.sessionId,
2409
- serverId: this.serverId,
2534
+ message: `Creating pending session ${this.config.sessionId} for connection setup`,
2535
+ sessionId: this.config.sessionId,
2536
+ serverId: this.config.serverId,
2410
2537
  timestamp: Date.now(),
2411
2538
  id: nanoid()
2412
2539
  });
2413
- await sessions.create({
2414
- sessionId: this.sessionId,
2415
- userId: this.userId,
2416
- serverId: this.serverId,
2417
- serverName: this.serverName,
2418
- serverUrl: this.serverUrl,
2419
- callbackUrl: this.callbackUrl,
2420
- transportType: this.transportType || "streamable-http",
2421
- headers: this.headers,
2422
- createdAt: this.createdAt,
2540
+ await this._store.create({
2541
+ ...this.session,
2423
2542
  updatedAt,
2424
2543
  status: "pending"
2425
2544
  });
2426
2545
  }
2546
+ if (!existingSession && this.oauthProvider instanceof StorageOAuthClientProvider) {
2547
+ await this.oauthProvider.initializeCredentials();
2548
+ }
2427
2549
  }
2428
2550
  /**
2429
2551
  * Saves current session state to the session store
@@ -2431,34 +2553,14 @@ var MCPClient = class {
2431
2553
  * @param status - Session lifecycle status used by storage cleanup
2432
2554
  * @private
2433
2555
  */
2434
- async saveSession(status = "active", existingSession) {
2435
- if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
2556
+ async saveSession(status = "active") {
2557
+ if (!this.config.sessionId || !this.config.serverId || !this.config.serverUrl || !this.config.callbackUrl) {
2436
2558
  return;
2437
2559
  }
2438
- const sessionData = {
2439
- sessionId: this.sessionId,
2440
- userId: this.userId,
2441
- serverId: this.serverId,
2442
- serverName: this.serverName,
2443
- serverUrl: this.serverUrl,
2444
- callbackUrl: this.callbackUrl,
2445
- transportType: this.transportType || "streamable-http",
2446
- headers: this.headers,
2447
- createdAt: this.createdAt || Date.now(),
2448
- updatedAt: Date.now(),
2560
+ await this._store.update(this.config.userId, this.config.sessionId, {
2561
+ ...this.session,
2449
2562
  status
2450
- };
2451
- if (status === "active") {
2452
- sessionData.authUrl = null;
2453
- }
2454
- if (existingSession === void 0) {
2455
- existingSession = await sessions.get(this.userId, this.sessionId);
2456
- }
2457
- if (existingSession) {
2458
- await sessions.update(this.userId, this.sessionId, sessionData);
2459
- } else {
2460
- await sessions.create(sessionData);
2461
- }
2563
+ });
2462
2564
  }
2463
2565
  /**
2464
2566
  * Removes transient setup/auth sessions without masking the original error.
@@ -2466,7 +2568,7 @@ var MCPClient = class {
2466
2568
  */
2467
2569
  async deleteTransientSession() {
2468
2570
  try {
2469
- await sessions.delete(this.userId, this.sessionId);
2571
+ await this._store.delete(this.config.userId, this.config.sessionId);
2470
2572
  } catch {
2471
2573
  }
2472
2574
  }
@@ -2476,7 +2578,7 @@ var MCPClient = class {
2476
2578
  * @private
2477
2579
  */
2478
2580
  async tryConnect() {
2479
- const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
2581
+ const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
2480
2582
  let lastError;
2481
2583
  for (const currentType of transportsToTry) {
2482
2584
  const isLastAttempt = currentType === transportsToTry[transportsToTry.length - 1];
@@ -2484,6 +2586,7 @@ var MCPClient = class {
2484
2586
  const transport = this.getTransport(currentType);
2485
2587
  this.transport = transport;
2486
2588
  await this.client.connect(transport);
2589
+ this._serverInfo = this.client.getServerVersion();
2487
2590
  return { transportType: currentType };
2488
2591
  } catch (error) {
2489
2592
  lastError = error;
@@ -2499,8 +2602,8 @@ var MCPClient = class {
2499
2602
  this._onObservabilityEvent.fire({
2500
2603
  level: "warn",
2501
2604
  message: `Transport ${currentType} failed, falling back`,
2502
- sessionId: this.sessionId,
2503
- serverId: this.serverId,
2605
+ sessionId: this.config.sessionId,
2606
+ serverId: this.config.serverId,
2504
2607
  metadata: {
2505
2608
  failedTransport: currentType,
2506
2609
  error: errorMessage
@@ -2543,15 +2646,15 @@ var MCPClient = class {
2543
2646
  try {
2544
2647
  this.emitStateChange("CONNECTING");
2545
2648
  const { transportType } = await this.tryConnect();
2546
- this.transportType = transportType;
2649
+ this.config.transportType = transportType;
2547
2650
  this.emitStateChange("CONNECTED");
2548
2651
  this.emitProgress("Connected successfully");
2549
2652
  this._onObservabilityEvent.fire({
2550
2653
  type: "mcp:client:session_saved",
2551
2654
  level: "info",
2552
- message: `Saving active session ${this.sessionId} (connect success)`,
2553
- sessionId: this.sessionId,
2554
- serverId: this.serverId,
2655
+ message: `Saving active session ${this.config.sessionId} (connect success)`,
2656
+ sessionId: this.config.sessionId,
2657
+ serverId: this.config.serverId,
2555
2658
  timestamp: Date.now(),
2556
2659
  id: nanoid()
2557
2660
  });
@@ -2574,23 +2677,23 @@ var MCPClient = class {
2574
2677
  this._onObservabilityEvent.fire({
2575
2678
  type: "mcp:client:session_saved",
2576
2679
  level: "info",
2577
- message: `Saving pending OAuth session ${this.sessionId}`,
2578
- sessionId: this.sessionId,
2579
- serverId: this.serverId,
2680
+ message: `Saving pending OAuth session ${this.config.sessionId}`,
2681
+ sessionId: this.config.sessionId,
2682
+ serverId: this.config.serverId,
2580
2683
  timestamp: Date.now(),
2581
2684
  id: nanoid()
2582
2685
  });
2583
2686
  await this.saveSession("pending");
2584
- if (this.serverId) {
2687
+ if (this.config.serverId) {
2585
2688
  this._onConnectionEvent.fire({
2586
2689
  type: "auth_required",
2587
- sessionId: this.sessionId,
2588
- serverId: this.serverId,
2690
+ sessionId: this.config.sessionId,
2691
+ serverId: this.config.serverId,
2589
2692
  authUrl,
2590
2693
  timestamp: Date.now()
2591
2694
  });
2592
- if (authUrl && this.onRedirect) {
2593
- this.onRedirect(authUrl);
2695
+ if (authUrl && this.config.onRedirect) {
2696
+ this.config.onRedirect(authUrl);
2594
2697
  }
2595
2698
  }
2596
2699
  throw new UnauthorizedError("OAuth authorization required");
@@ -2599,9 +2702,9 @@ var MCPClient = class {
2599
2702
  this.emitError(errorMessage, "connection");
2600
2703
  this.emitStateChange("FAILED");
2601
2704
  try {
2602
- const existingSession = await sessions.get(this.userId, this.sessionId);
2705
+ const existingSession = await this._store.get(this.config.userId, this.config.sessionId);
2603
2706
  if (!existingSession || existingSession.status !== "active") {
2604
- await sessions.delete(this.userId, this.sessionId);
2707
+ await this._store.delete(this.config.userId, this.config.sessionId);
2605
2708
  }
2606
2709
  } catch {
2607
2710
  }
@@ -2626,16 +2729,16 @@ var MCPClient = class {
2626
2729
  throw new Error(error);
2627
2730
  }
2628
2731
  if (state) {
2629
- const stateCheck = await this.oauthProvider.checkState(state);
2630
- if (!stateCheck.valid) {
2631
- const error = stateCheck.error || "Invalid OAuth state";
2632
- this.emitError(error, "auth");
2732
+ try {
2733
+ await this.oauthProvider.consumeState(state);
2734
+ } catch (error) {
2735
+ const msg = error instanceof Error ? error.message : "Invalid OAuth state";
2736
+ this.emitError(msg, "auth");
2633
2737
  this.emitStateChange("FAILED");
2634
- throw new Error(error);
2738
+ throw error;
2635
2739
  }
2636
- await this.oauthProvider.consumeState(state);
2637
2740
  }
2638
- const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
2741
+ const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
2639
2742
  let lastError;
2640
2743
  let tokensExchanged = false;
2641
2744
  let authenticatedStateEmitted = false;
@@ -2662,14 +2765,15 @@ var MCPClient = class {
2662
2765
  }
2663
2766
  }
2664
2767
  await this.client.connect(this.transport);
2665
- this.transportType = currentType;
2768
+ this._serverInfo = this.client.getServerVersion();
2769
+ this.config.transportType = currentType;
2666
2770
  this.emitStateChange("CONNECTED");
2667
2771
  this._onObservabilityEvent.fire({
2668
2772
  type: "mcp:client:session_saved",
2669
2773
  level: "info",
2670
- message: `Saving active session ${this.sessionId} (OAuth complete)`,
2671
- sessionId: this.sessionId,
2672
- serverId: this.serverId,
2774
+ message: `Saving active session ${this.config.sessionId} (OAuth complete)`,
2775
+ sessionId: this.config.sessionId,
2776
+ serverId: this.config.serverId,
2673
2777
  timestamp: Date.now(),
2674
2778
  id: nanoid()
2675
2779
  });
@@ -2742,11 +2846,11 @@ var MCPClient = class {
2742
2846
  this.emitStateChange("DISCOVERING");
2743
2847
  try {
2744
2848
  const result = await this.fetchTools();
2745
- if (this.serverId) {
2849
+ if (this.config.serverId) {
2746
2850
  this._onConnectionEvent.fire({
2747
2851
  type: "tools_discovered",
2748
- sessionId: this.sessionId,
2749
- serverId: this.serverId,
2852
+ sessionId: this.config.sessionId,
2853
+ serverId: this.config.serverId,
2750
2854
  toolCount: result.tools.length,
2751
2855
  tools: result.tools,
2752
2856
  timestamp: Date.now()
@@ -2786,8 +2890,8 @@ var MCPClient = class {
2786
2890
  level: "info",
2787
2891
  message: `Tool ${toolName} called successfully`,
2788
2892
  displayMessage: `Called tool ${toolName}`,
2789
- sessionId: this.sessionId,
2790
- serverId: this.serverId,
2893
+ sessionId: this.config.sessionId,
2894
+ serverId: this.config.serverId,
2791
2895
  payload: {
2792
2896
  toolName,
2793
2897
  args: toolArgs
@@ -2803,8 +2907,8 @@ var MCPClient = class {
2803
2907
  level: "error",
2804
2908
  message: errorMessage,
2805
2909
  displayMessage: `Failed to call tool ${toolName}`,
2806
- sessionId: this.sessionId,
2807
- serverId: this.serverId,
2910
+ sessionId: this.config.sessionId,
2911
+ serverId: this.config.serverId,
2808
2912
  payload: {
2809
2913
  errorType: "tool_execution",
2810
2914
  error: errorMessage,
@@ -2952,8 +3056,8 @@ var MCPClient = class {
2952
3056
  type: "mcp:client:error",
2953
3057
  level: "warn",
2954
3058
  message: "Initialization failed during clearSession",
2955
- sessionId: this.sessionId,
2956
- serverId: this.serverId,
3059
+ sessionId: this.config.sessionId,
3060
+ serverId: this.config.serverId,
2957
3061
  payload: { error: String(error) },
2958
3062
  timestamp: Date.now(),
2959
3063
  id: nanoid()
@@ -2962,7 +3066,7 @@ var MCPClient = class {
2962
3066
  if (this.oauthProvider) {
2963
3067
  await this.oauthProvider.invalidateCredentials("all");
2964
3068
  }
2965
- await sessions.delete(this.userId, this.sessionId);
3069
+ await this._store.delete(this.config.userId, this.config.sessionId);
2966
3070
  await this.disconnect();
2967
3071
  }
2968
3072
  /**
@@ -2996,19 +3100,19 @@ var MCPClient = class {
2996
3100
  }
2997
3101
  this.oauthProvider = null;
2998
3102
  this.transport = null;
2999
- if (this.serverId) {
3103
+ if (this.config.serverId) {
3000
3104
  this._onConnectionEvent.fire({
3001
3105
  type: "disconnected",
3002
- sessionId: this.sessionId,
3003
- serverId: this.serverId,
3106
+ sessionId: this.config.sessionId,
3107
+ serverId: this.config.serverId,
3004
3108
  timestamp: Date.now()
3005
3109
  });
3006
3110
  this._onObservabilityEvent.fire({
3007
3111
  type: "mcp:client:disconnect",
3008
3112
  level: "info",
3009
- message: `Disconnected from ${this.serverId}`,
3010
- sessionId: this.sessionId,
3011
- serverId: this.serverId,
3113
+ message: `Disconnected from ${this.config.serverId}`,
3114
+ sessionId: this.config.sessionId,
3115
+ serverId: this.config.serverId,
3012
3116
  payload: {},
3013
3117
  timestamp: Date.now(),
3014
3118
  id: nanoid()
@@ -3033,42 +3137,52 @@ var MCPClient = class {
3033
3137
  * @returns Server URL or empty string if not set
3034
3138
  */
3035
3139
  getServerUrl() {
3036
- return this.serverUrl || "";
3140
+ return this.config.serverUrl || "";
3037
3141
  }
3038
3142
  /**
3039
3143
  * Gets the OAuth callback URL
3040
3144
  * @returns Callback URL or empty string if not set
3041
3145
  */
3042
3146
  getCallbackUrl() {
3043
- return this.callbackUrl || "";
3147
+ return this.config.callbackUrl || "";
3044
3148
  }
3045
3149
  /**
3046
3150
  * Gets the transport type being used
3047
3151
  * @returns Transport type (defaults to 'streamable-http')
3048
3152
  */
3049
3153
  getTransportType() {
3050
- return this.transportType || "streamable-http";
3154
+ return this.config.transportType || "streamable-http";
3155
+ }
3156
+ /**
3157
+ * Gets the full server metadata from the MCP initialize response.
3158
+ * Includes name, version, icons, title, description, and website URL.
3159
+ * Returns undefined if the client hasn't connected yet.
3160
+ */
3161
+ getServerInfo() {
3162
+ return this._serverInfo;
3051
3163
  }
3052
3164
  /**
3053
- * Gets the human-readable server name
3165
+ * Gets the human-readable server name.
3166
+ * Prefers the server's reported title/name from the initialize response,
3167
+ * falling back to the name provided at construction or session metadata.
3054
3168
  * @returns Server name or undefined
3055
3169
  */
3056
3170
  getServerName() {
3057
- return this.serverName;
3171
+ return this.config.serverName;
3058
3172
  }
3059
3173
  /**
3060
3174
  * Gets the server ID
3061
3175
  * @returns Server ID or undefined
3062
3176
  */
3063
3177
  getServerId() {
3064
- return this.serverId;
3178
+ return this.config.serverId;
3065
3179
  }
3066
3180
  /**
3067
3181
  * Gets the session ID
3068
3182
  * @returns Session ID
3069
3183
  */
3070
3184
  getSessionId() {
3071
- return this.sessionId;
3185
+ return this.config.sessionId;
3072
3186
  }
3073
3187
  };
3074
3188
 
@@ -3091,6 +3205,14 @@ var ToolPolicyGateway = class {
3091
3205
  getServerId() {
3092
3206
  return this.client.getServerId?.();
3093
3207
  }
3208
+ /**
3209
+ * Returns the full server metadata from the underlying client, if available.
3210
+ * Includes name, version, icons, title, description, and website URL.
3211
+ * Available only after the client has connected and completed initialization.
3212
+ */
3213
+ getServerInfo() {
3214
+ return this.client.getServerInfo?.();
3215
+ }
3094
3216
  /**
3095
3217
  * Returns the human-readable server name from the underlying client, if available.
3096
3218
  */
@@ -3219,6 +3341,7 @@ var MultiSessionClient = class {
3219
3341
  __publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
3220
3342
  __publicField(this, "userId");
3221
3343
  __publicField(this, "options");
3344
+ __publicField(this, "_store");
3222
3345
  this.userId = userId;
3223
3346
  this.options = {
3224
3347
  timeout: DEFAULT_TIMEOUT_MS,
@@ -3226,6 +3349,7 @@ var MultiSessionClient = class {
3226
3349
  retryDelay: DEFAULT_RETRY_DELAY_MS,
3227
3350
  ...options
3228
3351
  };
3352
+ this._store = options.onObservabilityEvent ? withDbObservability(options.sessionStore ?? sessions, options.onObservabilityEvent) : options.sessionStore ?? sessions;
3229
3353
  }
3230
3354
  // -----------------------------------------------------------------------
3231
3355
  // Public API
@@ -3309,9 +3433,9 @@ var MultiSessionClient = class {
3309
3433
  * to querying the storage backend via `sessions.list(userId)`.
3310
3434
  */
3311
3435
  async fetchActiveSessions() {
3312
- const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await sessions.list(this.userId);
3436
+ const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await this._store.list(this.userId);
3313
3437
  return sessionList.filter(
3314
- (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
3438
+ (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active" && s.enabled !== false
3315
3439
  );
3316
3440
  }
3317
3441
  /**
@@ -3384,8 +3508,13 @@ var MultiSessionClient = class {
3384
3508
  callbackUrl: session.callbackUrl,
3385
3509
  serverName: session.serverName,
3386
3510
  transportType: session.transportType,
3387
- headers: session.headers
3511
+ headers: session.headers,
3512
+ hasSession: true,
3513
+ sessionStore: this._store
3388
3514
  });
3515
+ if (this.options.onObservabilityEvent) {
3516
+ client.onObservabilityEvent(this.options.onObservabilityEvent);
3517
+ }
3389
3518
  const timeoutMs = this.options.timeout;
3390
3519
  let timeoutTimer;
3391
3520
  const timeoutPromise = new Promise((_, reject) => {
@@ -3440,130 +3569,188 @@ function isConnectionEvent(event) {
3440
3569
  }
3441
3570
 
3442
3571
  // src/server/handlers/sse-handler.ts
3443
- var DEFAULT_HEARTBEAT_INTERVAL = 3e4;
3572
+ var DEFAULT_HEARTBEAT_MS = 3e4;
3444
3573
  function normalizeHeaders(headers) {
3445
3574
  if (!headers || typeof headers !== "object") return void 0;
3446
- const entries = Object.entries(headers).map(([key, value]) => [key.trim(), String(value).trim()]).filter(([key, value]) => key.length > 0 && value.length > 0);
3575
+ const entries = Object.entries(headers).map(([k, v]) => [k.trim(), String(v).trim()]).filter(([k, v]) => k.length > 0 && v.length > 0);
3447
3576
  return entries.length > 0 ? Object.fromEntries(entries) : void 0;
3448
3577
  }
3578
+ function validatingStateEvent(sessionId, session) {
3579
+ return {
3580
+ type: "state_changed",
3581
+ sessionId,
3582
+ serverId: session.serverId ?? "unknown",
3583
+ serverName: session.serverName ?? "Unknown",
3584
+ serverUrl: session.serverUrl,
3585
+ state: "VALIDATING",
3586
+ previousState: "DISCONNECTED",
3587
+ timestamp: Date.now()
3588
+ };
3589
+ }
3590
+ function connectionErrorEvent(sessionId, serverId, error, errorType) {
3591
+ return {
3592
+ type: "error",
3593
+ sessionId,
3594
+ serverId: serverId ?? "unknown",
3595
+ error: error instanceof Error ? error.message : "Connection failed",
3596
+ errorType,
3597
+ timestamp: Date.now()
3598
+ };
3599
+ }
3449
3600
  var SSEConnectionManager = class {
3601
+ // -----------------------------------------------------------------------
3602
+ // Constructor
3603
+ // -----------------------------------------------------------------------
3604
+ /**
3605
+ * @param options - Handler configuration (userId, auth, metadata, heartbeat, observability).
3606
+ * @param sendEvent - Callback that writes a typed event onto the SSE stream.
3607
+ */
3450
3608
  constructor(options, sendEvent) {
3451
3609
  this.options = options;
3452
3610
  this.sendEvent = sendEvent;
3611
+ // -----------------------------------------------------------------------
3612
+ // State
3613
+ // -----------------------------------------------------------------------
3453
3614
  __publicField(this, "userId");
3615
+ /** Active MCP transports keyed by sessionId. */
3454
3616
  __publicField(this, "clients", /* @__PURE__ */ new Map());
3617
+ /** Instrumented session store — always wraps `sessions` with DB observability. */
3618
+ __publicField(this, "observedStore");
3455
3619
  __publicField(this, "heartbeatTimer");
3456
- __publicField(this, "isActive", true);
3620
+ __publicField(this, "active", true);
3457
3621
  this.userId = options.userId;
3622
+ this.observedStore = withDbObservability(sessions, (event) => this.sendEvent(event));
3458
3623
  this.startHeartbeat();
3459
3624
  }
3625
+ // -----------------------------------------------------------------------
3626
+ // Public API
3627
+ // -----------------------------------------------------------------------
3460
3628
  /**
3461
- * Get resolved client metadata (dynamic > static > defaults)
3462
- */
3463
- async getResolvedClientMetadata(request) {
3464
- let metadata = {};
3465
- if (this.options.clientDefaults) {
3466
- metadata = { ...this.options.clientDefaults };
3467
- }
3468
- if (this.options.getClientMetadata) {
3469
- const dynamicMetadata = await this.options.getClientMetadata(request);
3470
- metadata = { ...metadata, ...dynamicMetadata };
3471
- }
3472
- return metadata;
3473
- }
3474
- /**
3475
- * Start heartbeat to keep connection alive
3476
- */
3477
- startHeartbeat() {
3478
- const interval = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL;
3479
- this.heartbeatTimer = setInterval(() => {
3480
- if (this.isActive) {
3481
- this.sendEvent({
3482
- level: "debug",
3483
- message: "heartbeat",
3484
- timestamp: Date.now()
3485
- });
3486
- }
3487
- }, interval);
3488
- }
3489
- /**
3490
- * Handle incoming RPC requests
3491
- * Returns the RPC response directly for immediate HTTP response (bypassing SSE latency)
3629
+ * Dispatches an incoming RPC request, emits timing observability,
3630
+ * and returns the response.
3631
+ *
3632
+ * Emits `rpc:start` before dispatch and `rpc:end` on completion
3633
+ * (success or error), each carrying the method name, sessionId, and
3634
+ * duration. All events flow through the unified `onObservability`
3635
+ * and the SSE stream.
3636
+ *
3637
+ * @param request - The deserialized RPC envelope.
3638
+ * @returns The RPC response (success or error).
3492
3639
  */
3493
3640
  async handleRequest(request) {
3641
+ const method = request.method;
3642
+ const sessionId = request.params?.sessionId;
3643
+ const t0 = performance.now();
3644
+ this.sendEvent({
3645
+ type: "rpc:start",
3646
+ level: "debug",
3647
+ message: method,
3648
+ sessionId,
3649
+ timestamp: Date.now()
3650
+ });
3494
3651
  try {
3495
- let result;
3496
- switch (request.method) {
3497
- case "listSessions":
3498
- result = await this.listSessions();
3499
- break;
3500
- case "connect":
3501
- result = await this.connect(request.params);
3502
- break;
3503
- case "reconnect":
3504
- result = await this.reconnect(request.params);
3505
- break;
3506
- case "disconnect":
3507
- result = await this.disconnect(request.params);
3508
- break;
3509
- case "listTools":
3510
- result = await this.listTools(request.params);
3511
- break;
3512
- case "setToolPolicy":
3513
- result = await this.setToolPolicy(request.params);
3514
- break;
3515
- case "getToolPolicy":
3516
- result = await this.getToolPolicy(request.params);
3517
- break;
3518
- case "callTool":
3519
- result = await this.callTool(request.params);
3520
- break;
3521
- case "getSession":
3522
- result = await this.getSession(request.params);
3523
- break;
3524
- case "finishAuth":
3525
- result = await this.finishAuth(request.params);
3526
- break;
3527
- case "listPrompts":
3528
- result = await this.listPrompts(request.params);
3529
- break;
3530
- case "getPrompt":
3531
- result = await this.getPrompt(request.params);
3532
- break;
3533
- case "listResources":
3534
- result = await this.listResources(request.params);
3535
- break;
3536
- case "readResource":
3537
- result = await this.readResource(request.params);
3538
- break;
3539
- default:
3540
- throw new Error(`Unknown method: ${request.method}`);
3541
- }
3542
- const response = {
3543
- id: request.id,
3544
- result
3545
- };
3652
+ const result = await this.dispatchImpl(request);
3653
+ this.sendEvent({
3654
+ type: "rpc:end",
3655
+ level: "debug",
3656
+ message: method,
3657
+ sessionId,
3658
+ payload: { durationMs: performance.now() - t0 },
3659
+ timestamp: Date.now()
3660
+ });
3661
+ const response = { id: request.id, result };
3546
3662
  this.sendEvent(response);
3547
3663
  return response;
3548
3664
  } catch (error) {
3549
- const errorResponse = {
3665
+ this.sendEvent({
3666
+ type: "rpc:end",
3667
+ level: "error",
3668
+ message: method,
3669
+ sessionId,
3670
+ payload: { durationMs: performance.now() - t0, error: String(error) },
3671
+ timestamp: Date.now()
3672
+ });
3673
+ const response = {
3550
3674
  id: request.id,
3551
3675
  error: {
3552
3676
  code: RpcErrorCodes.EXECUTION_ERROR,
3553
3677
  message: error instanceof Error ? error.message : "Unknown error"
3554
3678
  }
3555
3679
  };
3556
- this.sendEvent(errorResponse);
3557
- return errorResponse;
3680
+ this.sendEvent(response);
3681
+ return response;
3682
+ }
3683
+ }
3684
+ /**
3685
+ * Tears down all active MCP transports and stops the heartbeat timer.
3686
+ *
3687
+ * Disconnects are issued in parallel across all sessions. After calling
3688
+ * this method the manager instance should be discarded.
3689
+ */
3690
+ async dispose() {
3691
+ this.active = false;
3692
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
3693
+ await Promise.all(
3694
+ Array.from(this.clients.values()).map((c) => c.disconnect())
3695
+ );
3696
+ this.clients.clear();
3697
+ }
3698
+ // -----------------------------------------------------------------------
3699
+ // RPC Dispatch (raw — called by handleRequest which adds timing)
3700
+ // -----------------------------------------------------------------------
3701
+ /**
3702
+ * Routes an RPC method name to the appropriate private handler.
3703
+ *
3704
+ * @throws {Error} When the method name is unrecognized.
3705
+ */
3706
+ async dispatchImpl(request) {
3707
+ switch (request.method) {
3708
+ case "listSessions":
3709
+ return this.listSessions();
3710
+ case "connect":
3711
+ return this.connect(request.params);
3712
+ case "reconnect":
3713
+ return this.reconnect(request.params);
3714
+ case "disconnect":
3715
+ return this.disconnect(request.params);
3716
+ case "listTools":
3717
+ return this.listTools(request.params);
3718
+ case "setToolPolicy":
3719
+ return this.setToolPolicy(request.params);
3720
+ case "getToolPolicy":
3721
+ return this.getToolPolicy(request.params);
3722
+ case "updateSession":
3723
+ return this.updateSession(request.params);
3724
+ case "callTool":
3725
+ return this.callTool(request.params);
3726
+ case "getSession":
3727
+ return this.getSession(request.params);
3728
+ case "finishAuth":
3729
+ return this.finishAuth(request.params);
3730
+ case "listPrompts":
3731
+ return this.listPrompts(request.params);
3732
+ case "getPrompt":
3733
+ return this.getPrompt(request.params);
3734
+ case "listResources":
3735
+ return this.listResources(request.params);
3736
+ case "readResource":
3737
+ return this.readResource(request.params);
3738
+ default:
3739
+ throw new Error(`Unknown RPC method: ${request.method}`);
3558
3740
  }
3559
3741
  }
3742
+ // -----------------------------------------------------------------------
3743
+ // Session Query
3744
+ // -----------------------------------------------------------------------
3560
3745
  /**
3561
- * Get all sessions for the current userId
3746
+ * Lists all sessions owned by the current user.
3747
+ *
3748
+ * Returns a lightweight view — session metadata only, no credential fields.
3562
3749
  */
3563
3750
  async listSessions() {
3564
- const sessionList = await sessions.list(this.userId);
3751
+ const all = await sessions.list(this.userId);
3565
3752
  return {
3566
- sessions: sessionList.map((s) => ({
3753
+ sessions: all.map((s) => ({
3567
3754
  sessionId: s.sessionId,
3568
3755
  serverId: s.serverId,
3569
3756
  serverName: s.serverName,
@@ -3572,215 +3759,203 @@ var SSEConnectionManager = class {
3572
3759
  createdAt: s.createdAt,
3573
3760
  updatedAt: s.updatedAt ?? s.createdAt,
3574
3761
  status: s.status ?? "pending",
3575
- toolPolicy: s.toolPolicy
3762
+ toolPolicy: s.toolPolicy,
3763
+ enabled: s.enabled ?? true
3576
3764
  }))
3577
3765
  };
3578
3766
  }
3579
3767
  /**
3580
- * Connect to an MCP server
3768
+ * Restores and validates a previously persisted session.
3769
+ *
3770
+ * Loads the full session row (including credentials) from storage, creates
3771
+ * a new MCP transport, connects to the remote server, and emits the
3772
+ * discovered (policy-filtered) tool list via SSE.
3581
3773
  */
3582
- async connect(params) {
3583
- const { serverName, serverUrl, callbackUrl, transportType, clientId, clientSecret } = params;
3584
- const headers = normalizeHeaders(params.headers);
3585
- const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : generateServerId();
3586
- const existingSessions = await sessions.list(this.userId);
3587
- const duplicate = existingSessions.find(
3588
- (s) => s.serverId === serverId || s.serverUrl === serverUrl
3589
- );
3590
- if (duplicate) {
3591
- if (duplicate.status === "pending") {
3592
- await this.getSession({ sessionId: duplicate.sessionId });
3593
- return {
3594
- sessionId: duplicate.sessionId,
3595
- success: true
3596
- };
3597
- }
3598
- throw new Error(`Connection already exists for server: ${duplicate.serverUrl || duplicate.serverId} (${duplicate.serverName})`);
3599
- }
3600
- const sessionId = await sessions.generateSessionId();
3774
+ async getSession(params) {
3775
+ const session = await this.requireSession(params.sessionId);
3776
+ this.sendEvent(validatingStateEvent(params.sessionId, session));
3601
3777
  try {
3602
- const clientMetadata = await this.getResolvedClientMetadata();
3603
- const client = new MCPClient({
3604
- userId: this.userId,
3605
- sessionId,
3606
- serverId,
3607
- serverName,
3608
- serverUrl,
3609
- callbackUrl,
3610
- transportType,
3611
- headers,
3612
- clientId,
3613
- clientSecret,
3614
- ...clientMetadata
3615
- // Spread client metadata (clientName, clientUri, logoUri, policyUri)
3616
- });
3617
- this.clients.set(sessionId, client);
3618
- client.onConnectionEvent((event) => {
3619
- this.emitConnectionEvent(event);
3620
- });
3621
- client.onObservabilityEvent((event) => {
3622
- this.sendEvent(event);
3623
- });
3778
+ const client = this.restoreClient(session);
3779
+ this.attachClientEvents(client);
3624
3780
  await client.connect();
3625
- await this.listPolicyFilteredTools(sessionId);
3626
- return {
3627
- sessionId,
3628
- success: true
3629
- };
3781
+ this.clients.set(params.sessionId, client);
3782
+ const { result } = await this.listPolicyFilteredTools(params.sessionId);
3783
+ return { success: true, toolCount: result.tools.length };
3630
3784
  } catch (error) {
3631
- if (error instanceof UnauthorizedError) {
3632
- this.clients.delete(sessionId);
3633
- return {
3634
- sessionId,
3635
- success: true
3636
- };
3637
- }
3638
- this.emitConnectionEvent({
3639
- type: "error",
3640
- sessionId,
3641
- serverId,
3642
- error: error instanceof Error ? error.message : "Connection failed",
3643
- errorType: "connection",
3644
- timestamp: Date.now()
3645
- });
3646
- this.clients.delete(sessionId);
3785
+ this.sendEvent(connectionErrorEvent(params.sessionId, session.serverId, error, "validation"));
3647
3786
  throw error;
3648
3787
  }
3649
3788
  }
3789
+ // -----------------------------------------------------------------------
3790
+ // Connection Lifecycle
3791
+ // -----------------------------------------------------------------------
3650
3792
  /**
3651
- * Reconnect to an MCP server — tears down the active client transport/connection
3652
- * and creates a fresh connection while reusing the existing session credentials in a single RPC call.
3793
+ * Initiates a connection to a new MCP server.
3794
+ *
3795
+ * If a session for the same `serverId` or `serverUrl` already exists and
3796
+ * is still in a `pending` (OAuth) state, the existing session is resumed
3797
+ * instead of creating a duplicate.
3798
+ *
3799
+ * `UnauthorizedError` is treated as a pending-auth state and returned as
3800
+ * a successful result (the client will then redirect to the auth URL).
3653
3801
  */
3654
- async reconnect(params) {
3655
- const { serverId: rawServerId, serverName, serverUrl, callbackUrl, transportType, clientId, clientSecret } = params;
3802
+ async connect(params) {
3656
3803
  const headers = normalizeHeaders(params.headers);
3657
- const serverId = rawServerId && rawServerId.length <= 12 ? rawServerId : generateServerId();
3658
- const existingSessions = await sessions.list(this.userId);
3659
- const duplicate = existingSessions.find(
3660
- (s) => s.serverId === serverId || s.serverUrl === serverUrl
3661
- );
3662
- const sessionId = duplicate ? duplicate.sessionId : await sessions.generateSessionId();
3663
- if (duplicate) {
3664
- const existingClient = this.clients.get(duplicate.sessionId);
3665
- if (existingClient) {
3666
- await existingClient.disconnect();
3667
- this.clients.delete(duplicate.sessionId);
3804
+ const serverId = this.normalizeServerId(params.serverId);
3805
+ const existing = await this.findExistingSession(serverId, params.serverUrl);
3806
+ if (existing) {
3807
+ if (existing.status === "pending") {
3808
+ return this.getSession({ sessionId: existing.sessionId }).then(() => ({
3809
+ sessionId: existing.sessionId,
3810
+ success: true
3811
+ }));
3668
3812
  }
3813
+ throw new Error(
3814
+ `Connection already exists for server: ${existing.serverUrl ?? existing.serverId} (${existing.serverName})`
3815
+ );
3669
3816
  }
3670
- try {
3671
- const clientMetadata = await this.getResolvedClientMetadata();
3672
- const client = new MCPClient({
3673
- userId: this.userId,
3674
- sessionId,
3675
- serverId,
3676
- serverName,
3677
- serverUrl,
3678
- callbackUrl,
3679
- transportType,
3680
- headers,
3681
- clientId,
3682
- clientSecret,
3683
- ...clientMetadata
3684
- });
3685
- this.clients.set(sessionId, client);
3686
- client.onConnectionEvent((event) => {
3687
- this.emitConnectionEvent(event);
3688
- });
3689
- client.onObservabilityEvent((event) => {
3690
- this.sendEvent(event);
3691
- });
3692
- await client.connect();
3693
- await this.listPolicyFilteredTools(sessionId);
3694
- return { sessionId, success: true };
3695
- } catch (error) {
3696
- if (error instanceof UnauthorizedError) {
3697
- this.clients.delete(sessionId);
3698
- return { sessionId, success: true };
3817
+ const sessionId = await sessions.generateSessionId();
3818
+ const metadata = await this.getResolvedClientMetadata();
3819
+ const client = new MCPClient({
3820
+ userId: this.userId,
3821
+ sessionId,
3822
+ serverId,
3823
+ serverName: params.serverName,
3824
+ serverUrl: params.serverUrl,
3825
+ callbackUrl: params.callbackUrl,
3826
+ transportType: params.transportType,
3827
+ headers,
3828
+ sessionStore: this.observedStore,
3829
+ ...metadata
3830
+ });
3831
+ this.cacheClient(sessionId, client);
3832
+ return this.connectAndDiscover(client, sessionId, serverId);
3833
+ }
3834
+ /**
3835
+ * Reconnects to an MCP server by tearing down the active transport if one
3836
+ * exists and instantiating a fresh connection, reusing the existing session
3837
+ * (and its stored credentials / DCR client info) from the database.
3838
+ */
3839
+ async reconnect(params) {
3840
+ const headers = normalizeHeaders(params.headers);
3841
+ const serverId = this.normalizeServerId(params.serverId);
3842
+ const existing = await this.findExistingSession(serverId, params.serverUrl);
3843
+ const sessionId = existing ? existing.sessionId : await sessions.generateSessionId();
3844
+ if (existing) {
3845
+ const staleClient = this.clients.get(existing.sessionId);
3846
+ if (staleClient) {
3847
+ await staleClient.disconnect();
3848
+ this.clients.delete(existing.sessionId);
3699
3849
  }
3700
- this.emitConnectionEvent({
3701
- type: "error",
3702
- sessionId,
3703
- serverId,
3704
- error: error instanceof Error ? error.message : "Connection failed",
3705
- errorType: "connection",
3706
- timestamp: Date.now()
3707
- });
3708
- this.clients.delete(sessionId);
3709
- throw error;
3710
3850
  }
3851
+ const metadata = await this.getResolvedClientMetadata();
3852
+ const client = new MCPClient({
3853
+ userId: this.userId,
3854
+ sessionId,
3855
+ serverId,
3856
+ serverName: params.serverName,
3857
+ serverUrl: params.serverUrl,
3858
+ callbackUrl: params.callbackUrl,
3859
+ transportType: params.transportType,
3860
+ headers,
3861
+ sessionStore: this.observedStore,
3862
+ ...metadata
3863
+ });
3864
+ this.cacheClient(sessionId, client);
3865
+ return this.connectAndDiscover(client, sessionId, serverId);
3711
3866
  }
3712
3867
  /**
3713
- * Disconnect from an MCP server
3868
+ * Disconnects from an MCP server.
3869
+ *
3870
+ * If an active in-memory transport exists it delegates to `clearSession()`
3871
+ * (which sends the server an HTTP DELETE per the Streamable spec). If no
3872
+ * active transport is available (orphaned session from a failed OAuth flow),
3873
+ * the session row is removed directly from storage.
3714
3874
  */
3715
3875
  async disconnect(params) {
3716
- const { sessionId } = params;
3717
- const client = this.clients.get(sessionId);
3876
+ const client = this.clients.get(params.sessionId);
3718
3877
  if (client) {
3719
3878
  await client.clearSession();
3720
- this.clients.delete(sessionId);
3879
+ this.clients.delete(params.sessionId);
3721
3880
  } else {
3722
- await sessions.delete(this.userId, sessionId);
3881
+ await sessions.delete(this.userId, params.sessionId);
3723
3882
  }
3724
3883
  return { success: true };
3725
3884
  }
3885
+ // -----------------------------------------------------------------------
3886
+ // OAuth
3887
+ // -----------------------------------------------------------------------
3726
3888
  /**
3727
- * Get an existing client or create and connect a new one for the session.
3889
+ * Completes the OAuth 2.1 authorization code flow for a pending session.
3890
+ *
3891
+ * Loads the stored session (with credentials), creates a fresh `MCPClient`,
3892
+ * and calls `finishAuth` inside a {@link runWithCodeVerifierState} context
3893
+ * so the PKCE code verifier is available without a DB read.
3894
+ *
3895
+ * The session's stored `transportType` is intentionally **omitted** from the
3896
+ * client config — this lets `MCPClient` auto-negotiate (try Streamable HTTP
3897
+ * first, fall back to SSE), which is required for servers like Neon that
3898
+ * only support SSE transport.
3728
3899
  */
3729
- async getOrCreateClient(sessionId) {
3730
- const existing = this.clients.get(sessionId);
3731
- if (existing) {
3732
- return existing;
3733
- }
3734
- const session = await sessions.get(this.userId, sessionId);
3735
- if (!session) {
3736
- throw new Error("Session not found");
3900
+ async finishAuth(params) {
3901
+ const parsed = parseOAuthState(params.state);
3902
+ const sessionId = parsed?.sessionId ?? params.state;
3903
+ const session = await this.requireSession(sessionId);
3904
+ try {
3905
+ const clientId = session.clientId ?? void 0;
3906
+ const clientSecret = session.clientInformation?.client_secret ?? void 0;
3907
+ const client = new MCPClient({
3908
+ userId: this.userId,
3909
+ sessionId,
3910
+ serverId: session.serverId,
3911
+ serverName: session.serverName,
3912
+ serverUrl: session.serverUrl,
3913
+ callbackUrl: session.callbackUrl,
3914
+ headers: session.headers,
3915
+ clientId,
3916
+ clientSecret,
3917
+ hasSession: true,
3918
+ cachedCredentials: { tokens: session.tokens ?? void 0 },
3919
+ sessionStore: this.observedStore
3920
+ });
3921
+ this.attachClientEvents(client);
3922
+ await runWithCodeVerifierState(
3923
+ session.codeVerifier ?? "",
3924
+ "S256",
3925
+ () => client.finishAuth(params.code, params.state)
3926
+ );
3927
+ this.clients.set(sessionId, client);
3928
+ const { result } = await this.listPolicyFilteredTools(sessionId);
3929
+ return { success: true, toolCount: result.tools.length };
3930
+ } catch (error) {
3931
+ this.sendEvent(connectionErrorEvent(sessionId, session.serverId, error, "auth"));
3932
+ throw error;
3737
3933
  }
3738
- const client = new MCPClient({
3739
- userId: this.userId,
3740
- sessionId,
3741
- // These fields are optional in MCPClient, but when rehydrating a known
3742
- // stored session on the server we pass them explicitly to preserve the
3743
- // original transport/connection metadata instead of relying on lazy
3744
- // reloading during initialize().
3745
- serverId: session.serverId,
3746
- serverName: session.serverName,
3747
- serverUrl: session.serverUrl,
3748
- callbackUrl: session.callbackUrl,
3749
- transportType: session.transportType,
3750
- headers: session.headers
3751
- });
3752
- client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3753
- client.onObservabilityEvent((event) => this.sendEvent(event));
3754
- await client.connect();
3755
- this.clients.set(sessionId, client);
3756
- return client;
3757
3934
  }
3935
+ // -----------------------------------------------------------------------
3936
+ // Tool Discovery & Policy
3937
+ // -----------------------------------------------------------------------
3758
3938
  /**
3759
- * Fetches all tools from the remote MCP server and emits a `tools_discovered` event.
3939
+ * Fetches the complete tool list from the remote MCP server and emits a
3940
+ * `tools_discovered` SSE event with two lists:
3760
3941
  *
3761
- * Two lists are always published together:
3762
- * - `tools` policy-filtered list that agents are allowed to call.
3763
- * - `allTools` — the complete, unfiltered list used by the management UI so
3764
- * that blocked tools still appear as checkboxes in the dialog.
3942
+ * - `tools` — policy-filtered (what agents are allowed to call)
3943
+ * - `allTools` complete, unfiltered (used by the management UI)
3765
3944
  *
3766
- * `fetchTools()` is called first (populates the in-memory cache), then
3767
- * `gateway.listTools()` re-uses that cache internally — so only one remote
3768
- * network round-trip is made regardless of how many callers follow.
3945
+ * Only a single network round-trip is made: `fetchTools()` populates the
3946
+ * in-memory cache, then `gateway.listTools()` reuses that cache.
3769
3947
  *
3770
- * @param sessionId - The session whose tools should be discovered.
3771
- * @returns The session record and the policy-filtered tool list.
3772
- * @throws {Error} When the session does not exist in the store.
3948
+ * @param sessionId - The session to discover tools for.
3949
+ * @returns The session record and the filtered tool result.
3950
+ * @throws {Error} If the session does not exist in storage.
3773
3951
  */
3774
3952
  async listPolicyFilteredTools(sessionId) {
3775
- const session = await sessions.get(this.userId, sessionId);
3776
- if (!session) {
3777
- throw new Error("Session not found");
3778
- }
3953
+ const session = await this.requireSession(sessionId);
3779
3954
  const client = await this.getOrCreateClient(sessionId);
3780
3955
  const allTools = await client.fetchTools().catch(() => ({ tools: [] }));
3781
3956
  const gateway = createToolPolicyGateway(this.userId, sessionId, client);
3782
3957
  const result = await gateway.listTools();
3783
- this.emitConnectionEvent({
3958
+ this.sendEvent({
3784
3959
  type: "tools_discovered",
3785
3960
  sessionId,
3786
3961
  serverId: session.serverId ?? "unknown",
@@ -3792,254 +3967,254 @@ var SSEConnectionManager = class {
3792
3967
  return { session, result };
3793
3968
  }
3794
3969
  /**
3795
- * Returns the policy-filtered tool list for a session (agent-facing).
3796
- * Internally re-uses `listPolicyFilteredTools` which also emits a
3797
- * `tools_discovered` SSE event to keep client state up to date.
3970
+ * Returns the policy-filtered tool list for a session.
3971
+ *
3972
+ * Delegates to {@link listPolicyFilteredTools}, which also emits a
3973
+ * `tools_discovered` SSE event to keep the client state current.
3798
3974
  */
3799
3975
  async listTools(params) {
3800
- const { sessionId } = params;
3801
- const { result } = await this.listPolicyFilteredTools(sessionId);
3976
+ const { result } = await this.listPolicyFilteredTools(params.sessionId);
3802
3977
  return { tools: result.tools };
3803
3978
  }
3804
3979
  /**
3805
- * Get all raw tools with effective access state for management UI.
3980
+ * Returns all raw tools annotated with their effective policy state
3981
+ * for display in the management UI.
3806
3982
  */
3807
3983
  async getToolPolicy(params) {
3808
- const { sessionId } = params;
3809
- const session = await sessions.get(this.userId, sessionId);
3810
- if (!session) {
3811
- throw new Error("Session not found");
3812
- }
3813
- const client = await this.getOrCreateClient(sessionId);
3984
+ const session = await this.requireSession(params.sessionId);
3985
+ const client = await this.getOrCreateClient(params.sessionId);
3814
3986
  const allTools = await client.fetchTools();
3815
- const toolPolicy = session.toolPolicy ?? {
3987
+ const policy = session.toolPolicy ?? {
3816
3988
  mode: "all",
3817
3989
  toolIds: [],
3818
3990
  updatedAt: session.updatedAt ?? session.createdAt
3819
3991
  };
3820
3992
  const serverId = session.serverId ?? "unknown";
3821
- const tools = allTools.tools.map((tool) => ({
3822
- ...tool,
3823
- toolId: createToolId(serverId, tool.name),
3824
- allowed: isToolAllowed(toolPolicy, tool.name, session.serverId)
3993
+ const tools = allTools.tools.map((t) => ({
3994
+ ...t,
3995
+ toolId: createToolId(serverId, t.name),
3996
+ allowed: isToolAllowed(policy, t.name, session.serverId)
3825
3997
  }));
3826
3998
  return {
3827
- toolPolicy,
3999
+ toolPolicy: policy,
3828
4000
  tools,
3829
4001
  toolCount: tools.length,
3830
- allowedToolCount: tools.filter((tool) => tool.allowed).length
4002
+ allowedToolCount: tools.filter((t) => t.allowed).length
3831
4003
  };
3832
4004
  }
3833
4005
  /**
3834
- * Persists a new tool access policy for a session and broadcasts the updated
3835
- * filtered tool list to all connected browser clients via a `tools_discovered` event.
4006
+ * Persists a new tool access policy and broadcasts the updated filtered
4007
+ * tool list via a `tools_discovered` SSE event.
3836
4008
  *
3837
- * Both `tools` (policy-filtered) and `allTools` (complete list) are emitted
3838
- * so the management UI can immediately reflect the new checkbox states without
3839
- * an additional round-trip to the server.
3840
- *
3841
- * @param params - Session ID and the new `{ mode, toolIds }` policy to apply.
3842
- * @throws {Error} When the session does not exist or the policy references unknown tool IDs.
4009
+ * @throws {Error} If the session does not exist or the policy references
4010
+ * tool IDs that don't match any known tool.
3843
4011
  */
3844
4012
  async setToolPolicy(params) {
3845
- const { sessionId } = params;
3846
- const session = await sessions.get(this.userId, sessionId);
3847
- if (!session) {
3848
- throw new Error("Session not found");
3849
- }
3850
- const client = await this.getOrCreateClient(sessionId);
4013
+ const session = await this.requireSession(params.sessionId);
4014
+ const client = await this.getOrCreateClient(params.sessionId);
3851
4015
  const allTools = await client.fetchTools();
3852
- const toolPolicy = normalizeToolPolicyForUpdate(params.toolPolicy);
3853
- validateToolPolicyAgainstTools(toolPolicy, allTools.tools, session.serverId);
3854
- await sessions.update(this.userId, sessionId, { toolPolicy });
3855
- const filteredTools = createToolPolicyGateway(this.userId, sessionId, client).filterTools({ ...session, toolPolicy }, allTools.tools);
3856
- this.emitConnectionEvent({
4016
+ const policy = normalizeToolPolicyForUpdate(params.toolPolicy);
4017
+ validateToolPolicyAgainstTools(policy, allTools.tools, session.serverId);
4018
+ await sessions.update(this.userId, params.sessionId, { toolPolicy: policy });
4019
+ const filtered = createToolPolicyGateway(
4020
+ this.userId,
4021
+ params.sessionId,
4022
+ client
4023
+ ).filterTools({ ...session, toolPolicy: policy }, allTools.tools);
4024
+ this.sendEvent({
3857
4025
  type: "tools_discovered",
3858
- sessionId,
4026
+ sessionId: params.sessionId,
3859
4027
  serverId: session.serverId ?? "unknown",
3860
- toolCount: filteredTools.length,
3861
- tools: filteredTools,
4028
+ toolCount: filtered.length,
4029
+ tools: filtered,
3862
4030
  allTools: allTools.tools,
3863
4031
  timestamp: Date.now()
3864
4032
  });
3865
- return {
3866
- success: true,
3867
- toolPolicy,
3868
- tools: filteredTools,
3869
- toolCount: filteredTools.length
3870
- };
3871
- }
3872
- /**
3873
- * Proxies a tool invocation to the remote MCP server.
3874
- * Resolves the client for the given session and delegates to the tool router.
3875
- */
3876
- async callTool(params) {
3877
- const { sessionId, toolName, toolArgs } = params;
3878
- const client = await this.getOrCreateClient(sessionId);
3879
- const result = await createToolPolicyGateway(this.userId, sessionId, client).callTool(toolName, toolArgs);
3880
- const meta = result._meta || {};
3881
- return {
3882
- ...result,
3883
- _meta: {
3884
- ...meta,
3885
- sessionId
3886
- }
3887
- };
4033
+ return { success: true, toolPolicy: policy, tools: filtered, toolCount: filtered.length };
3888
4034
  }
3889
4035
  /**
3890
- * Restore and validate an existing session
4036
+ * Enables or disables a session for agent tool discovery.
4037
+ *
4038
+ * Disabled sessions retain their OAuth tokens and connection metadata
4039
+ * but are hidden from `MultiSessionClient.connect()` and blocked from
4040
+ * RPC tool access. Re-enabling does not require re-authentication.
4041
+ *
4042
+ * @param params - `{ sessionId, enabled: boolean }`
4043
+ * @returns `{ success: true }`
4044
+ * @throws {Error} If the session does not exist.
3891
4045
  */
3892
- async getSession(params) {
3893
- const { sessionId } = params;
3894
- const session = await sessions.get(this.userId, sessionId);
3895
- if (!session) {
3896
- throw new Error("Session not found");
3897
- }
3898
- this.emitConnectionEvent({
3899
- type: "state_changed",
3900
- sessionId,
3901
- serverId: session.serverId ?? "unknown",
3902
- serverName: session.serverName ?? "Unknown",
3903
- serverUrl: session.serverUrl,
3904
- state: "VALIDATING",
3905
- previousState: "DISCONNECTED",
3906
- timestamp: Date.now()
3907
- });
3908
- try {
3909
- const clientMetadata = await this.getResolvedClientMetadata();
3910
- const client = new MCPClient({
3911
- userId: this.userId,
3912
- sessionId,
3913
- // These fields are optional in MCPClient, but when rehydrating a known
3914
- // stored session on the server we pass them explicitly to preserve the
3915
- // original transport/connection metadata instead of relying on lazy
3916
- // reloading during initialize().
3917
- serverId: session.serverId,
3918
- serverName: session.serverName,
3919
- serverUrl: session.serverUrl,
3920
- callbackUrl: session.callbackUrl,
3921
- transportType: session.transportType,
3922
- headers: session.headers,
3923
- ...clientMetadata
3924
- });
3925
- client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3926
- client.onObservabilityEvent((event) => this.sendEvent(event));
3927
- await client.connect();
3928
- this.clients.set(sessionId, client);
3929
- const { result: tools } = await this.listPolicyFilteredTools(sessionId);
3930
- return { success: true, toolCount: tools.tools.length };
3931
- } catch (error) {
3932
- this.emitConnectionEvent({
3933
- type: "error",
3934
- sessionId,
3935
- serverId: session.serverId ?? "unknown",
3936
- error: error instanceof Error ? error.message : "Validation failed",
3937
- errorType: "validation",
3938
- timestamp: Date.now()
3939
- });
3940
- throw error;
3941
- }
4046
+ async updateSession(params) {
4047
+ await this.requireSession(params.sessionId);
4048
+ await sessions.update(this.userId, params.sessionId, { enabled: params.enabled });
4049
+ return { success: true };
3942
4050
  }
4051
+ // -----------------------------------------------------------------------
4052
+ // Tool Execution
4053
+ // -----------------------------------------------------------------------
3943
4054
  /**
3944
- * Complete OAuth authorization flow
4055
+ * Proxies a tool invocation to the remote MCP server.
4056
+ *
4057
+ * Resolves (or creates) the transport for the given session, runs the call
4058
+ * through the tool-policy gateway, and injects `sessionId` into the result
4059
+ * metadata so the client can route the response without scanning all sessions.
3945
4060
  */
3946
- async finishAuth(params) {
3947
- const { code } = params;
3948
- const oauthState = params.state;
3949
- const parsedState = parseOAuthState(oauthState);
3950
- const sessionId = parsedState?.sessionId || oauthState;
3951
- const session = await sessions.get(this.userId, sessionId);
3952
- if (!session) {
3953
- throw new Error("Session not found");
3954
- }
3955
- try {
3956
- const client = new MCPClient({
3957
- userId: this.userId,
3958
- sessionId,
3959
- // These fields are optional in MCPClient, but when rehydrating a known
3960
- // stored session on the server we pass them explicitly to preserve the
3961
- // original connection metadata instead of relying on lazy
3962
- // reloading during initialize().
3963
- serverId: session.serverId,
3964
- serverName: session.serverName,
3965
- serverUrl: session.serverUrl,
3966
- callbackUrl: session.callbackUrl,
3967
- // NOTE: transportType is intentionally omitted here.
3968
- // The session's stored transportType is a placeholder ('streamable-http')
3969
- // set before transport negotiation. Omitting it lets MCPClient auto-negotiate
3970
- // (try streamable_http → SSE fallback), which is critical for servers like
3971
- // Neon that only support SSE transport.
3972
- headers: session.headers
3973
- });
3974
- client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3975
- await client.finishAuth(code, oauthState);
3976
- this.clients.set(sessionId, client);
3977
- const { result: tools } = await this.listPolicyFilteredTools(sessionId);
3978
- return { success: true, toolCount: tools.tools.length };
3979
- } catch (error) {
3980
- this.emitConnectionEvent({
3981
- type: "error",
3982
- sessionId,
3983
- serverId: session.serverId ?? "unknown",
3984
- error: error instanceof Error ? error.message : "OAuth completion failed",
3985
- errorType: "auth",
3986
- timestamp: Date.now()
3987
- });
3988
- throw error;
3989
- }
4061
+ async callTool(params) {
4062
+ const client = await this.getOrCreateClient(params.sessionId);
4063
+ const result = await createToolPolicyGateway(
4064
+ this.userId,
4065
+ params.sessionId,
4066
+ client
4067
+ ).callTool(params.toolName, params.toolArgs);
4068
+ return { ...result, _meta: { ...result._meta ?? {}, sessionId: params.sessionId } };
3990
4069
  }
3991
- /**
3992
- * List prompts from a session
3993
- */
4070
+ // -----------------------------------------------------------------------
4071
+ // Prompts
4072
+ // -----------------------------------------------------------------------
4073
+ /** Lists all prompts exposed by the remote MCP server. */
3994
4074
  async listPrompts(params) {
3995
- const { sessionId } = params;
3996
- const client = await this.getOrCreateClient(sessionId);
4075
+ const client = await this.getOrCreateClient(params.sessionId);
3997
4076
  const result = await client.listPrompts();
3998
4077
  return { prompts: result.prompts };
3999
4078
  }
4000
- /**
4001
- * Get a specific prompt
4002
- */
4079
+ /** Retrieves a specific prompt by name with optional arguments. */
4003
4080
  async getPrompt(params) {
4004
- const { sessionId, name, args } = params;
4005
- const client = await this.getOrCreateClient(sessionId);
4006
- return await client.getPrompt(name, args);
4081
+ const client = await this.getOrCreateClient(params.sessionId);
4082
+ return client.getPrompt(params.name, params.args);
4007
4083
  }
4008
- /**
4009
- * List resources from a session
4010
- */
4084
+ // -----------------------------------------------------------------------
4085
+ // Resources
4086
+ // -----------------------------------------------------------------------
4087
+ /** Lists all resources exposed by the remote MCP server. */
4011
4088
  async listResources(params) {
4012
- const { sessionId } = params;
4013
- const client = await this.getOrCreateClient(sessionId);
4089
+ const client = await this.getOrCreateClient(params.sessionId);
4014
4090
  const result = await client.listResources();
4015
4091
  return { resources: result.resources };
4016
4092
  }
4093
+ /** Reads a specific resource identified by URI. */
4094
+ async readResource(params) {
4095
+ const client = await this.getOrCreateClient(params.sessionId);
4096
+ return client.readResource(params.uri);
4097
+ }
4098
+ // -----------------------------------------------------------------------
4099
+ // Internal Helpers
4100
+ // -----------------------------------------------------------------------
4101
+ /** Resolves client metadata: `getClientMetadata()` → `clientDefaults` → `{}`. */
4102
+ async getResolvedClientMetadata(request) {
4103
+ let metadata = this.options.clientDefaults ? { ...this.options.clientDefaults } : {};
4104
+ if (this.options.getClientMetadata) {
4105
+ metadata = { ...metadata, ...await this.options.getClientMetadata(request) };
4106
+ }
4107
+ return metadata;
4108
+ }
4109
+ /** Ensures the given session exists in storage and throws otherwise. */
4110
+ async requireSession(sessionId) {
4111
+ const session = await sessions.get(this.userId, sessionId, { includeCredentials: true });
4112
+ if (!session) throw new Error(`Session ${sessionId} not found`);
4113
+ return session;
4114
+ }
4115
+ /** Finds an existing session matching the given serverId or serverUrl. */
4116
+ async findExistingSession(serverId, serverUrl) {
4117
+ const all = await sessions.list(this.userId);
4118
+ return all.find((s) => s.serverId === serverId || s.serverUrl === serverUrl);
4119
+ }
4120
+ /** Normalizes a serverId to max 12 chars (DeepSeek/OpenAI 64-char tool-name limit). */
4121
+ normalizeServerId(raw) {
4122
+ return raw && raw.length <= 12 ? raw : generateServerId();
4123
+ }
4017
4124
  /**
4018
- * Read a specific resource
4125
+ * Returns the cached in-memory transport for `sessionId`, or creates one
4126
+ * from the persisted session row (with credentials) and connects it.
4019
4127
  */
4020
- async readResource(params) {
4021
- const { sessionId, uri } = params;
4022
- const client = await this.getOrCreateClient(sessionId);
4023
- return client.readResource(uri);
4128
+ async getOrCreateClient(sessionId) {
4129
+ const existing = this.clients.get(sessionId);
4130
+ if (existing) return existing;
4131
+ const session = await this.requireSession(sessionId);
4132
+ if (session.enabled === false) {
4133
+ throw new Error("Session is disabled \u2014 re-enable it via updateSession to access tools");
4134
+ }
4135
+ const client = this.restoreClient(session);
4136
+ this.attachClientEvents(client);
4137
+ await client.connect();
4138
+ this.clients.set(sessionId, client);
4139
+ return client;
4140
+ }
4141
+ /**
4142
+ * Builds an `MCPClient` from a stored session row.
4143
+ *
4144
+ * Extracts `clientId` and `clientSecret` from the session's credential
4145
+ * fields and passes `hasSession: true` + `cachedCredentials` so the client
4146
+ * can skip redundant existence checks and credential reads.
4147
+ */
4148
+ restoreClient(session) {
4149
+ const clientId = session.clientId ?? void 0;
4150
+ const clientSecret = session.clientInformation?.client_secret ?? void 0;
4151
+ return new MCPClient({
4152
+ userId: this.userId,
4153
+ sessionId: session.sessionId,
4154
+ serverId: session.serverId,
4155
+ serverName: session.serverName,
4156
+ serverUrl: session.serverUrl,
4157
+ callbackUrl: session.callbackUrl,
4158
+ transportType: session.transportType,
4159
+ headers: session.headers,
4160
+ clientId,
4161
+ clientSecret,
4162
+ hasSession: true,
4163
+ cachedCredentials: { tokens: session.tokens ?? void 0 },
4164
+ sessionStore: this.observedStore
4165
+ });
4024
4166
  }
4025
4167
  /**
4026
- * Emit connection event
4168
+ * Wires connection and observability events from the client into the
4169
+ * unified observability channel. Connection events go to `sendEvent`
4170
+ * (SSE stream); observability events go to `emitObs` (user callback
4171
+ * + SSE stream).
4027
4172
  */
4028
- emitConnectionEvent(event) {
4029
- this.sendEvent(event);
4173
+ attachClientEvents(client) {
4174
+ client.onConnectionEvent((e) => this.sendEvent(e));
4175
+ client.onObservabilityEvent((e) => this.sendEvent(e));
4030
4176
  }
4031
4177
  /**
4032
- * Cleanup and close all connections
4178
+ * Registers the client in the in-memory cache and attaches its event
4179
+ * listeners to the unified observability channel.
4033
4180
  */
4034
- async dispose() {
4035
- this.isActive = false;
4036
- if (this.heartbeatTimer) {
4037
- clearInterval(this.heartbeatTimer);
4181
+ cacheClient(sessionId, client) {
4182
+ this.attachClientEvents(client);
4183
+ this.clients.set(sessionId, client);
4184
+ }
4185
+ /**
4186
+ * Attempts a `client.connect()` and, on success, discovers tools.
4187
+ *
4188
+ * If the server responds with `UnauthorizedError`, the session is
4189
+ * treated as pending OAuth — a successful result is returned with
4190
+ * the sessionId so the browser client can redirect to the auth URL.
4191
+ *
4192
+ * For any other error an `error` connection event is emitted and the
4193
+ * client is removed from the in-memory cache before re-throwing.
4194
+ */
4195
+ async connectAndDiscover(client, sessionId, serverId) {
4196
+ try {
4197
+ await client.connect();
4198
+ await this.listPolicyFilteredTools(sessionId);
4199
+ return { sessionId, success: true };
4200
+ } catch (error) {
4201
+ if (error instanceof UnauthorizedError) {
4202
+ this.clients.delete(sessionId);
4203
+ return { sessionId, success: true };
4204
+ }
4205
+ this.sendEvent(connectionErrorEvent(sessionId, serverId, error, "connection"));
4206
+ this.clients.delete(sessionId);
4207
+ throw error;
4038
4208
  }
4039
- await Promise.all(
4040
- Array.from(this.clients.values()).map((client) => client.disconnect())
4041
- );
4042
- this.clients.clear();
4209
+ }
4210
+ /** Starts the periodic heartbeat timer. */
4211
+ startHeartbeat() {
4212
+ const ms = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS;
4213
+ this.heartbeatTimer = setInterval(() => {
4214
+ if (this.active) {
4215
+ this.sendEvent({ level: "debug", message: "heartbeat", timestamp: Date.now() });
4216
+ }
4217
+ }, ms);
4043
4218
  }
4044
4219
  };
4045
4220
  function createSSEHandler(options) {
@@ -4068,8 +4243,7 @@ function createSSEHandler(options) {
4068
4243
  });
4069
4244
  req.on("end", async () => {
4070
4245
  try {
4071
- const request = JSON.parse(body);
4072
- await manager.handleRequest(request);
4246
+ await manager.handleRequest(JSON.parse(body));
4073
4247
  } catch {
4074
4248
  }
4075
4249
  });
@@ -4236,6 +4410,6 @@ data: ${JSON.stringify(data)}
4236
4410
  return { GET, POST };
4237
4411
  }
4238
4412
 
4239
- export { MCPClient, MultiSessionClient, SSEConnectionManager, StorageOAuthClientProvider, UnauthorizedError, createNextMcpHandler, createSSEHandler, onSessionMutation, sanitizeServerLabel, sessions };
4413
+ export { MCPClient, MultiSessionClient, SSEConnectionManager, StorageOAuthClientProvider, UnauthorizedError, createNextMcpHandler, createSSEHandler, decryptObject, encryptObject, onSessionMutation, sanitizeServerLabel, sessions, withDbObservability };
4240
4414
  //# sourceMappingURL=index.mjs.map
4241
4415
  //# sourceMappingURL=index.mjs.map