@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
@@ -6,9 +6,10 @@ var streamableHttp_js = require('@modelcontextprotocol/sdk/client/streamableHttp
6
6
  var sse_js = require('@modelcontextprotocol/sdk/client/sse.js');
7
7
  var auth_js = require('@modelcontextprotocol/sdk/client/auth.js');
8
8
  var types_js = require('@modelcontextprotocol/sdk/types.js');
9
+ var async_hooks = require('async_hooks');
9
10
  var fs2 = require('fs');
10
11
  var path = require('path');
11
- var crypto = require('crypto');
12
+ var crypto$1 = require('crypto');
12
13
 
13
14
  function _interopNamespace(e) {
14
15
  if (e && e.__esModule) return e;
@@ -43,9 +44,9 @@ var __export = (target, all) => {
43
44
  };
44
45
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
45
46
 
46
- // node_modules/tsup/assets/cjs_shims.js
47
+ // ../../node_modules/tsup/assets/cjs_shims.js
47
48
  var init_cjs_shims = __esm({
48
- "node_modules/tsup/assets/cjs_shims.js"() {
49
+ "../../node_modules/tsup/assets/cjs_shims.js"() {
49
50
  }
50
51
  });
51
52
 
@@ -325,7 +326,6 @@ var RedisStorageBackend = class {
325
326
  constructor(redis2) {
326
327
  this.redis = redis2;
327
328
  __publicField(this, "KEY_PREFIX", "mcp:session:");
328
- __publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
329
329
  __publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
330
330
  __publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
331
331
  }
@@ -344,9 +344,6 @@ var RedisStorageBackend = class {
344
344
  getSessionKey(userId, sessionId) {
345
345
  return `${this.KEY_PREFIX}${userId}:${sessionId}`;
346
346
  }
347
- getCredentialsKey(userId, sessionId) {
348
- return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
349
- }
350
347
  /**
351
348
  * Generates Redis key for tracking all sessions for a user
352
349
  * @private
@@ -436,48 +433,54 @@ var RedisStorageBackend = class {
436
433
  }
437
434
  async patchCredentials(userId, sessionId, data) {
438
435
  const sessionKey = this.getSessionKey(userId, sessionId);
439
- const credentialsKey = this.getCredentialsKey(userId, sessionId);
440
- const sessionExists = await this.redis.exists(sessionKey);
441
- if (!sessionExists) {
436
+ const currentStr = await this.redis.get(sessionKey);
437
+ if (!currentStr) {
442
438
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
443
439
  }
444
- const session = await this.get(userId, sessionId);
445
- const currentTtl = await this.redis.ttl(sessionKey);
446
- const effectiveTtl = currentTtl > 0 && session ? currentTtl : resolveSessionRedisTtlSeconds(session ?? { status: "pending" });
447
- const currentStr = await this.redis.get(credentialsKey);
448
- const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
449
- const credentials = { ...current, ...data, sessionId, userId };
450
- if (effectiveTtl > 0) {
451
- await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
452
- } else {
453
- await this.redis.set(credentialsKey, JSON.stringify(credentials));
454
- }
440
+ const current = JSON.parse(currentStr);
441
+ const updated = { ...current, ...data, sessionId, userId };
442
+ const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
443
+ await this.redis.set(sessionKey, JSON.stringify(updated), "EX", effectiveTtl);
455
444
  }
456
- async get(userId, sessionId) {
445
+ async get(userId, sessionId, options) {
457
446
  try {
458
447
  const sessionKey = this.getSessionKey(userId, sessionId);
459
448
  const sessionDataStr = await this.redis.get(sessionKey);
460
- if (!sessionDataStr) {
461
- return null;
449
+ if (!sessionDataStr) return null;
450
+ const session = normalizeStoredSession(JSON.parse(sessionDataStr));
451
+ if (!options?.includeCredentials) {
452
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
453
+ return sessionOnly;
462
454
  }
463
- const session = JSON.parse(sessionDataStr);
464
- return normalizeStoredSession(session);
455
+ return session;
465
456
  } catch (error) {
466
457
  console.error("[RedisStorageBackend] Failed to get session:", error);
467
458
  return null;
468
459
  }
469
460
  }
470
461
  async getCredentials(userId, sessionId) {
471
- const session = await this.get(userId, sessionId);
462
+ const session = await this.get(userId, sessionId, { includeCredentials: true });
472
463
  if (!session) return null;
473
- const credentialsStr = await this.redis.get(this.getCredentialsKey(userId, sessionId));
474
- return credentialsStr ? JSON.parse(credentialsStr) : { sessionId, userId };
464
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
465
+ return {
466
+ sessionId,
467
+ userId,
468
+ clientInformation,
469
+ tokens,
470
+ codeVerifier,
471
+ codeVerifierChallenge,
472
+ codeVerifierNonce,
473
+ clientId,
474
+ oauthState
475
+ };
475
476
  }
476
477
  async clearCredentials(userId, sessionId) {
477
478
  await this.patchCredentials(userId, sessionId, {
478
479
  clientInformation: null,
479
480
  tokens: null,
480
481
  codeVerifier: null,
482
+ codeVerifierChallenge: null,
483
+ codeVerifierNonce: null,
481
484
  clientId: null,
482
485
  oauthState: null
483
486
  });
@@ -510,10 +513,9 @@ var RedisStorageBackend = class {
510
513
  async delete(userId, sessionId) {
511
514
  try {
512
515
  const sessionKey = this.getSessionKey(userId, sessionId);
513
- const credentialsKey = this.getCredentialsKey(userId, sessionId);
514
516
  const userIdKey = this.getUserIdKey(userId);
515
517
  await this.redis.srem(userIdKey, sessionId);
516
- await this.redis.del(sessionKey, credentialsKey);
518
+ await this.redis.del(sessionKey);
517
519
  } catch (error) {
518
520
  console.error("[RedisStorageBackend] Failed to remove session:", error);
519
521
  }
@@ -544,9 +546,8 @@ var RedisStorageBackend = class {
544
546
  async clearAll() {
545
547
  try {
546
548
  const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
547
- const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
548
549
  const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
549
- const allKeys = [...keys, ...credentialKeys, ...userIdKeys];
550
+ const allKeys = [...keys, ...userIdKeys];
550
551
  if (allKeys.length > 0) {
551
552
  await this.redis.del(...allKeys);
552
553
  }
@@ -570,7 +571,6 @@ var RedisStorageBackend = class {
570
571
  const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
571
572
  if (staleSessionIds.length > 0) {
572
573
  await this.redis.srem(userIdKey, ...staleSessionIds);
573
- await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
574
574
  }
575
575
  const remainingCount = await this.redis.scard(userIdKey);
576
576
  if (remainingCount === 0) {
@@ -596,7 +596,6 @@ var MemoryStorageBackend = class {
596
596
  constructor() {
597
597
  // Map<userId:sessionId, Session>
598
598
  __publicField(this, "sessions", /* @__PURE__ */ new Map());
599
- __publicField(this, "credentials", /* @__PURE__ */ new Map());
600
599
  // Map<userId, Set<sessionId>>
601
600
  __publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
602
601
  }
@@ -634,26 +633,46 @@ var MemoryStorageBackend = class {
634
633
  }
635
634
  async patchCredentials(userId, sessionId, data) {
636
635
  const sessionKey = this.getSessionKey(userId, sessionId);
637
- if (!this.sessions.has(sessionKey)) {
636
+ const current = this.sessions.get(sessionKey);
637
+ if (!current) {
638
638
  throw new Error(`Session ${sessionId} not found`);
639
639
  }
640
- const current = this.credentials.get(sessionKey) ?? { sessionId, userId };
641
- this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
640
+ this.sessions.set(sessionKey, { ...current, ...data, sessionId, userId });
642
641
  }
643
- async get(userId, sessionId) {
642
+ async get(userId, sessionId, options) {
644
643
  const sessionKey = this.getSessionKey(userId, sessionId);
645
- return this.sessions.get(sessionKey) || null;
644
+ const session = this.sessions.get(sessionKey) || null;
645
+ if (!session) return session;
646
+ if (!options?.includeCredentials) {
647
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
648
+ return sessionOnly;
649
+ }
650
+ return session;
646
651
  }
647
652
  async getCredentials(userId, sessionId) {
648
653
  const sessionKey = this.getSessionKey(userId, sessionId);
649
- if (!this.sessions.has(sessionKey)) return null;
650
- return this.credentials.get(sessionKey) ?? { sessionId, userId };
654
+ const session = this.sessions.get(sessionKey);
655
+ if (!session) return null;
656
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
657
+ return {
658
+ sessionId,
659
+ userId,
660
+ clientInformation,
661
+ tokens,
662
+ codeVerifier,
663
+ codeVerifierChallenge,
664
+ codeVerifierNonce,
665
+ clientId,
666
+ oauthState
667
+ };
651
668
  }
652
669
  async clearCredentials(userId, sessionId) {
653
670
  await this.patchCredentials(userId, sessionId, {
654
671
  clientInformation: null,
655
672
  tokens: null,
656
673
  codeVerifier: null,
674
+ codeVerifierChallenge: null,
675
+ codeVerifierNonce: null,
657
676
  clientId: null,
658
677
  oauthState: null
659
678
  });
@@ -677,7 +696,6 @@ var MemoryStorageBackend = class {
677
696
  async delete(userId, sessionId) {
678
697
  const sessionKey = this.getSessionKey(userId, sessionId);
679
698
  this.sessions.delete(sessionKey);
680
- this.credentials.delete(sessionKey);
681
699
  const set = this.userIdSessions.get(userId);
682
700
  if (set) {
683
701
  set.delete(sessionId);
@@ -691,14 +709,12 @@ var MemoryStorageBackend = class {
691
709
  }
692
710
  async clearAll() {
693
711
  this.sessions.clear();
694
- this.credentials.clear();
695
712
  this.userIdSessions.clear();
696
713
  }
697
714
  async cleanupExpired() {
698
715
  for (const [key, session] of this.sessions.entries()) {
699
716
  if (!isSessionExpired(session)) continue;
700
717
  this.sessions.delete(key);
701
- this.credentials.delete(key);
702
718
  const set = this.userIdSessions.get(session.userId);
703
719
  if (set) {
704
720
  set.delete(session.sessionId);
@@ -721,7 +737,6 @@ var FileStorageBackend = class {
721
737
  constructor(options = {}) {
722
738
  __publicField(this, "filePath");
723
739
  __publicField(this, "memoryCache", null);
724
- __publicField(this, "credentialsCache", null);
725
740
  __publicField(this, "initialized", false);
726
741
  this.filePath = options.path || "./sessions.json";
727
742
  }
@@ -736,22 +751,15 @@ var FileStorageBackend = class {
736
751
  const data = await fs2.promises.readFile(this.filePath, "utf-8");
737
752
  const json = JSON.parse(data);
738
753
  this.memoryCache = /* @__PURE__ */ new Map();
739
- this.credentialsCache = /* @__PURE__ */ new Map();
740
754
  if (Array.isArray(json.sessions)) {
741
755
  json.sessions.forEach((s) => {
742
756
  const session = normalizeStoredSession(s);
743
757
  this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
744
758
  });
745
759
  }
746
- if (Array.isArray(json.credentials)) {
747
- json.credentials.forEach((c) => {
748
- this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
749
- });
750
- }
751
760
  } catch (error) {
752
761
  if (error.code === "ENOENT") {
753
762
  this.memoryCache = /* @__PURE__ */ new Map();
754
- this.credentialsCache = /* @__PURE__ */ new Map();
755
763
  await this.flush();
756
764
  } else {
757
765
  console.error("[FileStorage] Failed to load sessions:", error);
@@ -765,10 +773,9 @@ var FileStorageBackend = class {
765
773
  if (!this.initialized) await this.init();
766
774
  }
767
775
  async flush() {
768
- if (!this.memoryCache || !this.credentialsCache) return;
776
+ if (!this.memoryCache) return;
769
777
  await fs2.promises.writeFile(this.filePath, JSON.stringify({
770
- sessions: Array.from(this.memoryCache.values()),
771
- credentials: Array.from(this.credentialsCache.values())
778
+ sessions: Array.from(this.memoryCache.values())
772
779
  }, null, 2), "utf-8");
773
780
  }
774
781
  getSessionKey(userId, sessionId) {
@@ -803,29 +810,49 @@ var FileStorageBackend = class {
803
810
  async patchCredentials(userId, sessionId, data) {
804
811
  await this.ensureInitialized();
805
812
  const sessionKey = this.getSessionKey(userId, sessionId);
806
- if (!this.memoryCache.has(sessionKey)) {
813
+ const current = this.memoryCache.get(sessionKey);
814
+ if (!current) {
807
815
  throw new Error(`Session ${sessionId} not found`);
808
816
  }
809
- const current = this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
810
- this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
817
+ this.memoryCache.set(sessionKey, { ...current, ...data, sessionId, userId });
811
818
  await this.flush();
812
819
  }
813
- async get(userId, sessionId) {
820
+ async get(userId, sessionId, options) {
814
821
  await this.ensureInitialized();
815
822
  const sessionKey = this.getSessionKey(userId, sessionId);
816
- return this.memoryCache.get(sessionKey) || null;
823
+ const session = this.memoryCache.get(sessionKey) || null;
824
+ if (!session) return null;
825
+ if (!options?.includeCredentials) {
826
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
827
+ return sessionOnly;
828
+ }
829
+ return session;
817
830
  }
818
831
  async getCredentials(userId, sessionId) {
819
832
  await this.ensureInitialized();
820
833
  const sessionKey = this.getSessionKey(userId, sessionId);
821
- if (!this.memoryCache.has(sessionKey)) return null;
822
- return this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
834
+ const session = this.memoryCache.get(sessionKey);
835
+ if (!session) return null;
836
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
837
+ return {
838
+ sessionId,
839
+ userId,
840
+ clientInformation,
841
+ tokens,
842
+ codeVerifier,
843
+ codeVerifierChallenge,
844
+ codeVerifierNonce,
845
+ clientId,
846
+ oauthState
847
+ };
823
848
  }
824
849
  async clearCredentials(userId, sessionId) {
825
850
  await this.patchCredentials(userId, sessionId, {
826
851
  clientInformation: null,
827
852
  tokens: null,
828
853
  codeVerifier: null,
854
+ codeVerifierChallenge: null,
855
+ codeVerifierNonce: null,
829
856
  clientId: null,
830
857
  oauthState: null
831
858
  });
@@ -842,7 +869,6 @@ var FileStorageBackend = class {
842
869
  await this.ensureInitialized();
843
870
  const sessionKey = this.getSessionKey(userId, sessionId);
844
871
  const deleted = this.memoryCache.delete(sessionKey);
845
- this.credentialsCache.delete(sessionKey);
846
872
  if (deleted) {
847
873
  await this.flush();
848
874
  }
@@ -854,7 +880,6 @@ var FileStorageBackend = class {
854
880
  async clearAll() {
855
881
  await this.ensureInitialized();
856
882
  this.memoryCache.clear();
857
- this.credentialsCache.clear();
858
883
  await this.flush();
859
884
  }
860
885
  async cleanupExpired() {
@@ -863,7 +888,6 @@ var FileStorageBackend = class {
863
888
  for (const [key, session] of this.memoryCache.entries()) {
864
889
  if (!isSessionExpired(session)) continue;
865
890
  this.memoryCache.delete(key);
866
- this.credentialsCache.delete(key);
867
891
  changed = true;
868
892
  }
869
893
  if (changed) {
@@ -880,12 +904,10 @@ var SqliteStorage = class {
880
904
  constructor(options = {}) {
881
905
  __publicField(this, "db", null);
882
906
  __publicField(this, "table");
883
- __publicField(this, "credentialsTable");
884
907
  __publicField(this, "initialized", false);
885
908
  __publicField(this, "dbPath");
886
909
  this.dbPath = options.path || "./sessions.db";
887
910
  this.table = options.table || "mcp_sessions";
888
- this.credentialsTable = `${this.table}_credentials`;
889
911
  }
890
912
  async init() {
891
913
  if (this.initialized) return;
@@ -905,13 +927,6 @@ var SqliteStorage = class {
905
927
  expiresAt INTEGER
906
928
  );
907
929
  CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
908
- CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
909
- sessionId TEXT NOT NULL,
910
- userId TEXT NOT NULL,
911
- data TEXT NOT NULL,
912
- PRIMARY KEY (userId, sessionId),
913
- FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
914
- );
915
930
  `);
916
931
  this.initialized = true;
917
932
  console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
@@ -968,41 +983,54 @@ var SqliteStorage = class {
968
983
  }
969
984
  async patchCredentials(userId, sessionId, data) {
970
985
  this.ensureInitialized();
971
- if (!await this.get(userId, sessionId)) {
986
+ const currentSession = await this.get(userId, sessionId, { includeCredentials: true });
987
+ if (!currentSession) {
972
988
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
973
989
  }
974
- const current = await this.getCredentials(userId, sessionId) ?? { sessionId, userId };
975
- const credentials = { ...current, ...data, sessionId, userId };
990
+ const updated = { ...currentSession, ...data, sessionId, userId };
976
991
  const stmt = this.db.prepare(
977
- `INSERT INTO ${this.credentialsTable} (sessionId, userId, data)
978
- VALUES (?, ?, ?)
979
- ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
992
+ `UPDATE ${this.table} SET data = ? WHERE sessionId = ? AND userId = ?`
980
993
  );
981
- stmt.run(sessionId, userId, JSON.stringify(credentials));
994
+ stmt.run(JSON.stringify(updated), sessionId, userId);
982
995
  }
983
- async get(userId, sessionId) {
996
+ async get(userId, sessionId, options) {
984
997
  this.ensureInitialized();
985
998
  const stmt = this.db.prepare(
986
999
  `SELECT data FROM ${this.table} WHERE sessionId = ? AND userId = ?`
987
1000
  );
988
1001
  const row = stmt.get(sessionId, userId);
989
1002
  if (!row) return null;
990
- return normalizeStoredSession(JSON.parse(row.data));
1003
+ const session = normalizeStoredSession(JSON.parse(row.data));
1004
+ if (!options?.includeCredentials) {
1005
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
1006
+ return sessionOnly;
1007
+ }
1008
+ return session;
991
1009
  }
992
1010
  async getCredentials(userId, sessionId) {
993
1011
  this.ensureInitialized();
994
- if (!await this.get(userId, sessionId)) return null;
995
- const stmt = this.db.prepare(
996
- `SELECT data FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
997
- );
998
- const row = stmt.get(sessionId, userId);
999
- return row ? JSON.parse(row.data) : { sessionId, userId };
1012
+ const session = await this.get(userId, sessionId, { includeCredentials: true });
1013
+ if (!session) return null;
1014
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
1015
+ return {
1016
+ sessionId,
1017
+ userId,
1018
+ clientInformation,
1019
+ tokens,
1020
+ codeVerifier,
1021
+ codeVerifierChallenge,
1022
+ codeVerifierNonce,
1023
+ clientId,
1024
+ oauthState
1025
+ };
1000
1026
  }
1001
1027
  async clearCredentials(userId, sessionId) {
1002
1028
  await this.patchCredentials(userId, sessionId, {
1003
1029
  clientInformation: null,
1004
1030
  tokens: null,
1005
1031
  codeVerifier: null,
1032
+ codeVerifierChallenge: null,
1033
+ codeVerifierNonce: null,
1006
1034
  clientId: null,
1007
1035
  oauthState: null
1008
1036
  });
@@ -1028,9 +1056,6 @@ var SqliteStorage = class {
1028
1056
  const stmt = this.db.prepare(
1029
1057
  `DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
1030
1058
  );
1031
- this.db.prepare(
1032
- `DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
1033
- ).run(sessionId, userId);
1034
1059
  stmt.run(sessionId, userId);
1035
1060
  }
1036
1061
  async listAllIds() {
@@ -1041,7 +1066,6 @@ var SqliteStorage = class {
1041
1066
  }
1042
1067
  async clearAll() {
1043
1068
  this.ensureInitialized();
1044
- this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
1045
1069
  const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
1046
1070
  stmt.run();
1047
1071
  }
@@ -1055,14 +1079,6 @@ var SqliteStorage = class {
1055
1079
  deleteStmt.run(row.sessionId, row.userId);
1056
1080
  }
1057
1081
  }
1058
- this.db.prepare(
1059
- `DELETE FROM ${this.credentialsTable}
1060
- WHERE NOT EXISTS (
1061
- SELECT 1 FROM ${this.table}
1062
- WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
1063
- AND ${this.table}.userId = ${this.credentialsTable}.userId
1064
- )`
1065
- ).run();
1066
1082
  }
1067
1083
  async disconnect() {
1068
1084
  if (this.db) {
@@ -1103,8 +1119,8 @@ function encryptObject(data) {
1103
1119
  }
1104
1120
  try {
1105
1121
  const text = JSON.stringify(data);
1106
- const iv = crypto.randomBytes(IV_LENGTH);
1107
- const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
1122
+ const iv = crypto$1.randomBytes(IV_LENGTH);
1123
+ const cipher = crypto$1.createCipheriv(ALGORITHM, key, iv);
1108
1124
  let encrypted = cipher.update(text, "utf-8", "hex");
1109
1125
  encrypted += cipher.final("hex");
1110
1126
  const authTag = cipher.getAuthTag().toString("hex");
@@ -1132,7 +1148,7 @@ function decryptObject(data) {
1132
1148
  const iv = Buffer.from(parts[2], "hex");
1133
1149
  const authTag = Buffer.from(parts[3], "hex");
1134
1150
  const encryptedText = parts[4];
1135
- const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
1151
+ const decipher = crypto$1.createDecipheriv(ALGORITHM, key, iv);
1136
1152
  decipher.setAuthTag(authTag);
1137
1153
  let decrypted = decipher.update(encryptedText, "hex", "utf-8");
1138
1154
  decrypted += decipher.final("utf-8");
@@ -1150,7 +1166,6 @@ var SupabaseStorageBackend = class {
1150
1166
  }
1151
1167
  async init() {
1152
1168
  await this.assertTable("mcp_sessions", "session_id");
1153
- await this.assertTable("mcp_credentials", "session_id");
1154
1169
  console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
1155
1170
  }
1156
1171
  async assertTable(table, column) {
@@ -1181,18 +1196,13 @@ var SupabaseStorageBackend = class {
1181
1196
  headers: decryptObject(row.headers),
1182
1197
  authUrl: row.auth_url,
1183
1198
  status: row.status ?? "pending",
1184
- toolPolicy: normalizeToolPolicy(row.tool_policy)
1185
- };
1186
- }
1187
- mapRowToCredentials(row, userId, sessionId) {
1188
- return {
1189
- sessionId,
1190
- userId,
1191
- clientInformation: decryptObject(row?.client_information),
1192
- tokens: decryptObject(row?.tokens),
1193
- codeVerifier: decryptObject(row?.code_verifier),
1194
- clientId: row?.client_id,
1195
- oauthState: row?.oauth_state
1199
+ toolPolicy: normalizeToolPolicy(row.tool_policy),
1200
+ clientInformation: decryptObject(row.client_information),
1201
+ tokens: decryptObject(row.tokens),
1202
+ codeVerifier: decryptObject(row.code_verifier),
1203
+ clientId: row.client_id,
1204
+ oauthState: row.oauth_state,
1205
+ enabled: row.enabled ?? true
1196
1206
  };
1197
1207
  }
1198
1208
  hasCredentialData(data) {
@@ -1250,6 +1260,7 @@ var SupabaseStorageBackend = class {
1250
1260
  if ("headers" in data) updateData.headers = encryptObject(data.headers);
1251
1261
  if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
1252
1262
  if ("toolPolicy" in data) updateData.tool_policy = normalizeToolPolicy(data.toolPolicy);
1263
+ if ("enabled" in data) updateData.enabled = data.enabled;
1253
1264
  const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
1254
1265
  let updatedRows = null;
1255
1266
  if (shouldUpdateSession) {
@@ -1271,23 +1282,22 @@ var SupabaseStorageBackend = class {
1271
1282
  }
1272
1283
  async patchCredentials(userId, sessionId, data) {
1273
1284
  if (!this.hasCredentialData(data)) return;
1274
- const row = {
1275
- user_id: userId,
1276
- session_id: sessionId,
1285
+ const updateData = {
1277
1286
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
1278
1287
  };
1279
- if ("clientInformation" in data) row.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
1280
- if ("tokens" in data) row.tokens = data.tokens == null ? null : encryptObject(data.tokens);
1281
- if ("codeVerifier" in data) row.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
1282
- if ("clientId" in data) row.client_id = data.clientId ?? null;
1283
- if ("oauthState" in data) row.oauth_state = data.oauthState ?? null;
1284
- const { error } = await this.supabase.from("mcp_credentials").upsert(row, { onConflict: "user_id,session_id" });
1288
+ if ("clientInformation" in data) updateData.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
1289
+ if ("tokens" in data) updateData.tokens = data.tokens == null ? null : encryptObject(data.tokens);
1290
+ if ("codeVerifier" in data) updateData.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
1291
+ if ("clientId" in data) updateData.client_id = data.clientId ?? null;
1292
+ if ("oauthState" in data) updateData.oauth_state = data.oauthState ?? null;
1293
+ const { error } = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId);
1285
1294
  if (error) {
1286
1295
  throw new Error(`Failed to update credentials: ${error.message}`);
1287
1296
  }
1288
1297
  }
1289
- async get(userId, sessionId) {
1290
- const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
1298
+ async get(userId, sessionId, options) {
1299
+ 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";
1300
+ const { data, error } = await this.supabase.from("mcp_sessions").select(selection).eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
1291
1301
  if (error) {
1292
1302
  console.error("[SupabaseStorage] Failed to get session:", error);
1293
1303
  return null;
@@ -1296,19 +1306,21 @@ var SupabaseStorageBackend = class {
1296
1306
  return this.mapRowToSessionData(data);
1297
1307
  }
1298
1308
  async getCredentials(userId, sessionId) {
1299
- const { data, error } = await this.supabase.from("mcp_credentials").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
1309
+ 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();
1300
1310
  if (error) {
1301
1311
  console.error("[SupabaseStorage] Failed to get credentials:", error);
1302
1312
  return null;
1303
1313
  }
1304
- if (data) {
1305
- return this.mapRowToCredentials(data, userId, sessionId);
1306
- }
1307
- const { data: sessionRows, error: sessionError } = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
1308
- if (sessionError || !sessionRows || sessionRows.length === 0) {
1309
- return null;
1310
- }
1311
- return { sessionId, userId };
1314
+ if (!data) return null;
1315
+ return {
1316
+ sessionId,
1317
+ userId,
1318
+ clientInformation: decryptObject(data.client_information),
1319
+ tokens: decryptObject(data.tokens),
1320
+ codeVerifier: decryptObject(data.code_verifier),
1321
+ clientId: data.client_id,
1322
+ oauthState: data.oauth_state
1323
+ };
1312
1324
  }
1313
1325
  async list(userId) {
1314
1326
  const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
@@ -1319,7 +1331,14 @@ var SupabaseStorageBackend = class {
1319
1331
  return data.map((row) => this.mapRowToSessionData(row));
1320
1332
  }
1321
1333
  async clearCredentials(userId, sessionId) {
1322
- const { error } = await this.supabase.from("mcp_credentials").delete().eq("user_id", userId).eq("session_id", sessionId);
1334
+ const { error } = await this.supabase.from("mcp_sessions").update({
1335
+ client_information: null,
1336
+ tokens: null,
1337
+ code_verifier: null,
1338
+ client_id: null,
1339
+ oauth_state: null,
1340
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1341
+ }).eq("user_id", userId).eq("session_id", sessionId);
1323
1342
  if (error) {
1324
1343
  throw new Error(`Failed to clear credentials: ${error.message}`);
1325
1344
  }
@@ -1347,10 +1366,6 @@ var SupabaseStorageBackend = class {
1347
1366
  return data.map((row) => row.session_id);
1348
1367
  }
1349
1368
  async clearAll() {
1350
- const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
1351
- if (credentialsError) {
1352
- console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
1353
- }
1354
1369
  const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
1355
1370
  if (error) {
1356
1371
  console.error("[SupabaseStorage] Failed to clear sessions:", error);
@@ -1377,16 +1392,12 @@ var NeonStorageBackend = class {
1377
1392
  constructor(sql, options = {}) {
1378
1393
  this.sql = sql;
1379
1394
  __publicField(this, "tableName");
1380
- __publicField(this, "credentialsTableName");
1381
1395
  const schema = options.schema || "public";
1382
1396
  const table = options.table || "mcp_sessions";
1383
- const credentialsTable = options.credentialsTable || "mcp_credentials";
1384
1397
  this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
1385
- this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
1386
1398
  }
1387
1399
  async init() {
1388
1400
  await this.assertTable(this.tableName, "mcp_sessions");
1389
- await this.assertTable(this.credentialsTableName, "mcp_credentials");
1390
1401
  console.log("[mcp-ts][Storage] Neon: storage tables verified.");
1391
1402
  }
1392
1403
  async assertTable(qualifiedName, displayName) {
@@ -1424,18 +1435,13 @@ var NeonStorageBackend = class {
1424
1435
  headers: decryptObject(row.headers),
1425
1436
  authUrl: row.auth_url ?? void 0,
1426
1437
  status: row.status ?? "pending",
1427
- toolPolicy: normalizeToolPolicy(row.tool_policy)
1428
- };
1429
- }
1430
- mapRowToCredentials(row, userId, sessionId) {
1431
- return {
1432
- sessionId,
1433
- userId,
1438
+ toolPolicy: normalizeToolPolicy(row.tool_policy),
1434
1439
  clientInformation: decryptObject(row.client_information),
1435
1440
  tokens: decryptObject(row.tokens),
1436
1441
  codeVerifier: decryptObject(row.code_verifier),
1437
1442
  clientId: row.client_id ?? void 0,
1438
- oauthState: row.oauth_state
1443
+ oauthState: row.oauth_state,
1444
+ enabled: row.enabled ?? true
1439
1445
  };
1440
1446
  }
1441
1447
  hasCredentialData(data) {
@@ -1505,7 +1511,7 @@ var NeonStorageBackend = class {
1505
1511
  const updatedSession = { ...currentSession, ...data };
1506
1512
  const status = updatedSession.status ?? "pending";
1507
1513
  const expiresAt = resolveSessionExpiresAt(status);
1508
- 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;
1514
+ 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;
1509
1515
  if (shouldUpdateSession) {
1510
1516
  const setClauses = [];
1511
1517
  const values = [];
@@ -1528,6 +1534,9 @@ var NeonStorageBackend = class {
1528
1534
  const toolPolicy = normalizeToolPolicy(updatedSession.toolPolicy, policyUpdatedAt) ?? { mode: "all", toolIds: [], updatedAt: policyUpdatedAt };
1529
1535
  addSet("tool_policy", toolPolicy);
1530
1536
  }
1537
+ if ("enabled" in data) {
1538
+ addSet("enabled", updatedSession.enabled);
1539
+ }
1531
1540
  setClauses.push("updated_at = now()");
1532
1541
  const updatedRows = await this.sql.query(
1533
1542
  `UPDATE ${this.tableName}
@@ -1543,45 +1552,45 @@ var NeonStorageBackend = class {
1543
1552
  }
1544
1553
  async patchCredentials(userId, sessionId, data) {
1545
1554
  if (!this.hasCredentialData(data)) return;
1546
- await this.sql.query(
1547
- `INSERT INTO ${this.credentialsTableName} (
1548
- user_id,
1549
- session_id,
1550
- client_information,
1551
- tokens,
1552
- code_verifier,
1553
- client_id,
1554
- oauth_state,
1555
- updated_at
1556
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, now())
1557
- ON CONFLICT (user_id, session_id)
1558
- DO UPDATE SET
1559
- client_information = CASE WHEN $8 THEN EXCLUDED.client_information ELSE ${this.credentialsTableName}.client_information END,
1560
- tokens = CASE WHEN $9 THEN EXCLUDED.tokens ELSE ${this.credentialsTableName}.tokens END,
1561
- code_verifier = CASE WHEN $10 THEN EXCLUDED.code_verifier ELSE ${this.credentialsTableName}.code_verifier END,
1562
- client_id = CASE WHEN $11 THEN EXCLUDED.client_id ELSE ${this.credentialsTableName}.client_id END,
1563
- oauth_state = CASE WHEN $12 THEN EXCLUDED.oauth_state ELSE ${this.credentialsTableName}.oauth_state END,
1564
- updated_at = now()`,
1565
- [
1566
- userId,
1567
- sessionId,
1568
- "clientInformation" in data ? data.clientInformation == null ? null : encryptObject(data.clientInformation) : null,
1569
- "tokens" in data ? data.tokens == null ? null : encryptObject(data.tokens) : null,
1570
- "codeVerifier" in data ? data.codeVerifier == null ? null : encryptObject(data.codeVerifier) : null,
1571
- "clientId" in data ? data.clientId ?? null : null,
1572
- "oauthState" in data ? data.oauthState ?? null : null,
1573
- "clientInformation" in data,
1574
- "tokens" in data,
1575
- "codeVerifier" in data,
1576
- "clientId" in data,
1577
- "oauthState" in data
1578
- ]
1555
+ const setClauses = [];
1556
+ const values = [];
1557
+ let paramIndex = 1;
1558
+ const addSet = (column, value) => {
1559
+ setClauses.push(`${column} = $${paramIndex++}`);
1560
+ values.push(value);
1561
+ };
1562
+ if ("clientInformation" in data) {
1563
+ addSet("client_information", data.clientInformation == null ? null : encryptObject(data.clientInformation));
1564
+ }
1565
+ if ("tokens" in data) {
1566
+ addSet("tokens", data.tokens == null ? null : encryptObject(data.tokens));
1567
+ }
1568
+ if ("codeVerifier" in data) {
1569
+ addSet("code_verifier", data.codeVerifier == null ? null : encryptObject(data.codeVerifier));
1570
+ }
1571
+ if ("clientId" in data) {
1572
+ addSet("client_id", data.clientId ?? null);
1573
+ }
1574
+ if ("oauthState" in data) {
1575
+ addSet("oauth_state", data.oauthState ?? null);
1576
+ }
1577
+ setClauses.push("updated_at = now()");
1578
+ const updatedRows = await this.sql.query(
1579
+ `UPDATE ${this.tableName}
1580
+ SET ${setClauses.join(", ")}
1581
+ WHERE user_id = $${paramIndex++} AND session_id = $${paramIndex++}
1582
+ RETURNING id`,
1583
+ [...values, userId, sessionId]
1579
1584
  );
1585
+ if (updatedRows.length === 0) {
1586
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
1587
+ }
1580
1588
  }
1581
- async get(userId, sessionId) {
1589
+ async get(userId, sessionId, options) {
1582
1590
  try {
1591
+ 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";
1583
1592
  const rows = await this.sql.query(
1584
- `SELECT * FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1593
+ `SELECT ${selection} FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1585
1594
  [userId, sessionId]
1586
1595
  );
1587
1596
  if (!rows[0]) return null;
@@ -1593,18 +1602,22 @@ var NeonStorageBackend = class {
1593
1602
  }
1594
1603
  async getCredentials(userId, sessionId) {
1595
1604
  try {
1596
- const credentialRows = await this.sql.query(
1597
- `SELECT * FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
1598
- [userId, sessionId]
1599
- );
1600
- if (credentialRows[0]) {
1601
- return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
1602
- }
1603
- const sessionRows = await this.sql.query(
1604
- `SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1605
+ const rows = await this.sql.query(
1606
+ `SELECT client_information, tokens, code_verifier, client_id, oauth_state
1607
+ FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1605
1608
  [userId, sessionId]
1606
1609
  );
1607
- return sessionRows[0] ? { sessionId, userId } : null;
1610
+ if (!rows[0]) return null;
1611
+ const row = rows[0];
1612
+ return {
1613
+ sessionId,
1614
+ userId,
1615
+ clientInformation: decryptObject(row.client_information),
1616
+ tokens: decryptObject(row.tokens),
1617
+ codeVerifier: decryptObject(row.code_verifier),
1618
+ clientId: row.client_id ?? void 0,
1619
+ oauthState: row.oauth_state
1620
+ };
1608
1621
  } catch (error) {
1609
1622
  console.error("[NeonStorage] Failed to get credentials:", error);
1610
1623
  return null;
@@ -1625,7 +1638,9 @@ var NeonStorageBackend = class {
1625
1638
  async clearCredentials(userId, sessionId) {
1626
1639
  try {
1627
1640
  await this.sql.query(
1628
- `DELETE FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
1641
+ `UPDATE ${this.tableName}
1642
+ SET client_information = null, tokens = null, code_verifier = null, client_id = null, oauth_state = null, updated_at = now()
1643
+ WHERE user_id = $1 AND session_id = $2`,
1629
1644
  [userId, sessionId]
1630
1645
  );
1631
1646
  } catch (error) {
@@ -1667,7 +1682,6 @@ var NeonStorageBackend = class {
1667
1682
  }
1668
1683
  async clearAll() {
1669
1684
  try {
1670
- await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
1671
1685
  await this.sql.query(`DELETE FROM ${this.tableName}`);
1672
1686
  } catch (error) {
1673
1687
  console.error("[NeonStorage] Failed to clear sessions:", error);
@@ -1912,6 +1926,60 @@ function onSessionMutation(listener) {
1912
1926
  sessionMutationListeners.delete(listener);
1913
1927
  };
1914
1928
  }
1929
+ function withDbObservability(store, emit) {
1930
+ const readMethods = /* @__PURE__ */ new Set([
1931
+ "get",
1932
+ "getCredentials",
1933
+ "list",
1934
+ "listIds",
1935
+ "listAllIds"
1936
+ ]);
1937
+ return new Proxy(store, {
1938
+ get(target, prop, receiver) {
1939
+ const original = Reflect.get(target, prop, receiver);
1940
+ if (typeof original !== "function") return original;
1941
+ return (...args) => {
1942
+ const method = prop;
1943
+ const isRead = readMethods.has(prop);
1944
+ const start = performance.now();
1945
+ const emitEvent = (error) => {
1946
+ emit({
1947
+ type: isRead ? "db:read" : "db:write",
1948
+ level: error ? "error" : "debug",
1949
+ message: `${method}(${args.map(
1950
+ (a) => typeof a === "string" ? a.slice(0, 24) : typeof a
1951
+ ).join(", ")})`,
1952
+ sessionId: typeof args[1] === "string" ? args[1] : void 0,
1953
+ payload: {
1954
+ method,
1955
+ argTypes: args.map((a) => typeof a),
1956
+ durationMs: performance.now() - start,
1957
+ ...error ? { error } : {}
1958
+ },
1959
+ timestamp: Date.now()
1960
+ });
1961
+ };
1962
+ try {
1963
+ const result = original.apply(target, args);
1964
+ if (result instanceof Promise) {
1965
+ return result.then((r) => {
1966
+ emitEvent();
1967
+ return r;
1968
+ }).catch((e) => {
1969
+ emitEvent(String(e));
1970
+ throw e;
1971
+ });
1972
+ }
1973
+ emitEvent();
1974
+ return result;
1975
+ } catch (e) {
1976
+ emitEvent(String(e));
1977
+ throw e;
1978
+ }
1979
+ };
1980
+ }
1981
+ });
1982
+ }
1915
1983
  var sessions = new Proxy({}, {
1916
1984
  get(_target, prop) {
1917
1985
  return async (...args) => {
@@ -1931,6 +1999,24 @@ var sessions = new Proxy({}, {
1931
1999
  });
1932
2000
 
1933
2001
  // src/server/mcp/storage-oauth-provider.ts
2002
+ var codeVerifierContext = new async_hooks.AsyncLocalStorage();
2003
+ function runWithCodeVerifierState(verifier, method, fn) {
2004
+ return codeVerifierContext.run({ verifier, method }, fn);
2005
+ }
2006
+ function base64UrlEncode(bytes) {
2007
+ let binary = "";
2008
+ for (const byte of bytes) {
2009
+ binary += String.fromCharCode(byte);
2010
+ }
2011
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2012
+ }
2013
+ async function createCodeChallenge(verifier) {
2014
+ const digest = await crypto.subtle.digest(
2015
+ "SHA-256",
2016
+ new TextEncoder().encode(verifier)
2017
+ );
2018
+ return base64UrlEncode(new Uint8Array(digest));
2019
+ }
1934
2020
  var StorageOAuthClientProvider = class {
1935
2021
  /**
1936
2022
  * Creates a new session-backed OAuth provider
@@ -1946,8 +2032,12 @@ var StorageOAuthClientProvider = class {
1946
2032
  __publicField(this, "logoUri");
1947
2033
  __publicField(this, "policyUri");
1948
2034
  __publicField(this, "clientSecret");
2035
+ __publicField(this, "_store");
1949
2036
  __publicField(this, "_authUrl");
1950
2037
  __publicField(this, "_clientId");
2038
+ __publicField(this, "_cachedCodeVerifier");
2039
+ __publicField(this, "_hasCodeVerifier", false);
2040
+ __publicField(this, "_cachedTokens");
1951
2041
  __publicField(this, "onRedirectCallback");
1952
2042
  this.userId = options.userId;
1953
2043
  this.serverId = options.serverId;
@@ -1959,6 +2049,8 @@ var StorageOAuthClientProvider = class {
1959
2049
  this.policyUri = options.policyUri;
1960
2050
  this._clientId = options.clientId;
1961
2051
  this.clientSecret = options.clientSecret;
2052
+ this._cachedTokens = options.cachedTokens;
2053
+ this._store = options.sessionStore ?? sessions;
1962
2054
  this.onRedirectCallback = options.onRedirect;
1963
2055
  }
1964
2056
  get clientMetadata() {
@@ -1986,7 +2078,7 @@ var StorageOAuthClientProvider = class {
1986
2078
  * @private
1987
2079
  */
1988
2080
  async getCredentials() {
1989
- const data = await sessions.getCredentials(this.userId, this.sessionId);
2081
+ const data = await this._store.getCredentials(this.userId, this.sessionId);
1990
2082
  if (!data) {
1991
2083
  return { userId: this.userId, sessionId: this.sessionId };
1992
2084
  }
@@ -1999,35 +2091,33 @@ var StorageOAuthClientProvider = class {
1999
2091
  * @throws Error if session doesn't exist (session must be created by controller layer)
2000
2092
  */
2001
2093
  async patchCredentials(data) {
2002
- await sessions.patchCredentials(this.userId, this.sessionId, data);
2094
+ await this._store.patchCredentials(this.userId, this.sessionId, data);
2003
2095
  }
2004
2096
  /**
2005
- * Retrieves stored OAuth client information
2097
+ * Retrieves stored OAuth client information.
2006
2098
  */
2007
2099
  async clientInformation() {
2008
- const data = await this.getCredentials();
2009
- if (data.clientId && !this._clientId) {
2010
- this._clientId = data.clientId;
2011
- }
2012
- if (this._clientId && !data.clientId) {
2013
- await this.patchCredentials({
2014
- clientId: this._clientId,
2015
- clientInformation: {
2016
- client_id: this._clientId,
2017
- ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2018
- }
2019
- });
2100
+ if (this._clientId) {
2101
+ return {
2102
+ client_id: this._clientId,
2103
+ ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2104
+ };
2020
2105
  }
2021
- if (data.clientInformation) {
2022
- return data.clientInformation;
2106
+ const data = await this.getCredentials();
2107
+ if (this._cachedTokens === void 0) {
2108
+ this._cachedTokens = data.tokens ?? null;
2023
2109
  }
2024
- if (!this._clientId) {
2025
- return void 0;
2110
+ if (data.clientId) {
2111
+ this._clientId = data.clientId;
2112
+ if (data.clientInformation) {
2113
+ return data.clientInformation;
2114
+ }
2115
+ return {
2116
+ client_id: data.clientId,
2117
+ ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2118
+ };
2026
2119
  }
2027
- return {
2028
- client_id: this._clientId,
2029
- ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2030
- };
2120
+ return void 0;
2031
2121
  }
2032
2122
  /**
2033
2123
  * Stores OAuth client information
@@ -2044,11 +2134,30 @@ var StorageOAuthClientProvider = class {
2044
2134
  */
2045
2135
  async saveTokens(tokens) {
2046
2136
  await this.patchCredentials({ tokens });
2137
+ this._cachedTokens = tokens;
2138
+ }
2139
+ /**
2140
+ * Persists static client credentials to DB immediately on creation.
2141
+ * Ensures getOrCreateClient() finds them on rehydration, even when the
2142
+ * server allows anonymous connect and never triggers DCR.
2143
+ */
2144
+ async initializeCredentials() {
2145
+ if (this._clientId) {
2146
+ await this.patchCredentials({
2147
+ clientId: this._clientId,
2148
+ clientInformation: {
2149
+ client_id: this._clientId,
2150
+ ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2151
+ }
2152
+ });
2153
+ }
2047
2154
  }
2048
2155
  get authUrl() {
2049
2156
  return this._authUrl;
2050
2157
  }
2051
2158
  async state() {
2159
+ this._cachedCodeVerifier = void 0;
2160
+ this._hasCodeVerifier = false;
2052
2161
  const nonce = nanoid.nanoid(32);
2053
2162
  await this.patchCredentials({
2054
2163
  oauthState: {
@@ -2069,7 +2178,7 @@ var StorageOAuthClientProvider = class {
2069
2178
  if (parsed.sessionId !== this.sessionId) {
2070
2179
  return { valid: false, error: "OAuth state mismatch" };
2071
2180
  }
2072
- const data = await sessions.getCredentials(this.userId, parsed.sessionId);
2181
+ const data = await this._store.getCredentials(this.userId, parsed.sessionId);
2073
2182
  if (!data) {
2074
2183
  return { valid: false, error: "Session not found" };
2075
2184
  }
@@ -2094,49 +2203,77 @@ var StorageOAuthClientProvider = class {
2094
2203
  }
2095
2204
  async redirectToAuthorization(authUrl) {
2096
2205
  this._authUrl = authUrl.toString();
2097
- await sessions.update(this.userId, this.sessionId, { authUrl: this._authUrl });
2206
+ const codeChallenge = authUrl.searchParams.get("code_challenge");
2207
+ const state = authUrl.searchParams.get("state");
2208
+ if (this._cachedCodeVerifier && codeChallenge && state) {
2209
+ const expectedChallenge = await createCodeChallenge(this._cachedCodeVerifier);
2210
+ if (expectedChallenge === codeChallenge) {
2211
+ const parsed = parseOAuthState(state);
2212
+ if (parsed) {
2213
+ await this.patchCredentials({
2214
+ codeVerifier: this._cachedCodeVerifier
2215
+ });
2216
+ }
2217
+ }
2218
+ }
2098
2219
  if (this.onRedirectCallback) {
2099
2220
  this.onRedirectCallback(authUrl.toString());
2100
2221
  }
2101
2222
  }
2102
2223
  async invalidateCredentials(scope) {
2103
2224
  if (scope === "all") {
2104
- await sessions.delete(this.userId, this.sessionId);
2225
+ this._cachedTokens = void 0;
2226
+ await this._store.delete(this.userId, this.sessionId);
2105
2227
  } else {
2106
2228
  const updates = {};
2107
2229
  if (scope === "client") {
2108
2230
  updates.clientInformation = null;
2109
2231
  updates.clientId = null;
2110
2232
  } else if (scope === "tokens") {
2233
+ this._cachedTokens = void 0;
2111
2234
  updates.tokens = null;
2112
2235
  } else if (scope === "verifier") {
2236
+ this._cachedCodeVerifier = void 0;
2237
+ this._hasCodeVerifier = false;
2113
2238
  updates.codeVerifier = null;
2114
2239
  }
2115
2240
  await this.patchCredentials(updates);
2116
2241
  }
2117
2242
  }
2118
2243
  async saveCodeVerifier(verifier) {
2119
- const data = await this.getCredentials();
2120
- if (data.codeVerifier) {
2244
+ if (this._hasCodeVerifier) {
2121
2245
  return;
2122
2246
  }
2123
- await this.patchCredentials({ codeVerifier: verifier });
2247
+ this._cachedCodeVerifier = verifier;
2248
+ this._hasCodeVerifier = true;
2124
2249
  }
2125
2250
  async codeVerifier() {
2126
- const data = await this.getCredentials();
2127
- if (data.clientId && !this._clientId) {
2128
- this._clientId = data.clientId;
2251
+ if (this._cachedCodeVerifier) {
2252
+ return this._cachedCodeVerifier;
2129
2253
  }
2130
- if (!data.codeVerifier) {
2131
- throw new Error("No code verifier found");
2254
+ const ctx = codeVerifierContext.getStore();
2255
+ if (ctx?.verifier) {
2256
+ return ctx.verifier;
2257
+ }
2258
+ const data = await this.getCredentials();
2259
+ if (data.codeVerifier) {
2260
+ return data.codeVerifier;
2132
2261
  }
2133
- return data.codeVerifier;
2262
+ throw new Error("No code verifier found");
2134
2263
  }
2135
2264
  async deleteCodeVerifier() {
2136
- await this.patchCredentials({ codeVerifier: null });
2265
+ this._cachedCodeVerifier = void 0;
2266
+ this._hasCodeVerifier = false;
2267
+ await this.patchCredentials({
2268
+ codeVerifier: null
2269
+ });
2137
2270
  }
2138
2271
  async tokens() {
2272
+ if (this._cachedTokens !== void 0) {
2273
+ return this._cachedTokens ?? void 0;
2274
+ }
2139
2275
  const data = await this.getCredentials();
2276
+ this._cachedTokens = data.tokens ?? null;
2140
2277
  if (data.clientId && !this._clientId) {
2141
2278
  this._clientId = data.clientId;
2142
2279
  }
@@ -2231,23 +2368,10 @@ var MCPClient = class {
2231
2368
  __publicField(this, "client");
2232
2369
  __publicField(this, "oauthProvider", null);
2233
2370
  __publicField(this, "transport", null);
2234
- __publicField(this, "userId");
2235
- __publicField(this, "serverId");
2236
- __publicField(this, "sessionId");
2237
- __publicField(this, "serverName");
2238
- __publicField(this, "transportType");
2239
- __publicField(this, "serverUrl");
2240
- __publicField(this, "callbackUrl");
2241
- __publicField(this, "onRedirect");
2242
- __publicField(this, "clientId");
2243
- __publicField(this, "clientSecret");
2244
- __publicField(this, "headers");
2245
- /** OAuth Client Metadata */
2246
- __publicField(this, "clientName");
2247
- __publicField(this, "clientUri");
2248
- __publicField(this, "logoUri");
2249
- __publicField(this, "policyUri");
2371
+ __publicField(this, "config");
2250
2372
  __publicField(this, "createdAt");
2373
+ __publicField(this, "_serverInfo");
2374
+ __publicField(this, "_store");
2251
2375
  /** Event emitters for connection lifecycle */
2252
2376
  __publicField(this, "_onConnectionEvent", new Emitter());
2253
2377
  __publicField(this, "onConnectionEvent", this._onConnectionEvent.event);
@@ -2263,21 +2387,8 @@ var MCPClient = class {
2263
2387
  * the memory when the client is no longer needed.
2264
2388
  */
2265
2389
  __publicField(this, "cachedTools", null);
2266
- this.serverUrl = options.serverUrl;
2267
- this.serverName = options.serverName;
2268
- this.callbackUrl = options.callbackUrl;
2269
- this.onRedirect = options.onRedirect;
2270
- this.userId = options.userId;
2271
- this.serverId = options.serverId;
2272
- this.sessionId = options.sessionId;
2273
- this.transportType = options.transportType;
2274
- this.clientId = options.clientId;
2275
- this.clientSecret = options.clientSecret;
2276
- this.headers = options.headers;
2277
- this.clientName = options.clientName;
2278
- this.clientUri = options.clientUri;
2279
- this.logoUri = options.logoUri;
2280
- this.policyUri = options.policyUri;
2390
+ this.config = { ...options };
2391
+ this._store = options.sessionStore ?? sessions;
2281
2392
  this.client = new index_js.Client(
2282
2393
  {
2283
2394
  name: MCP_CLIENT_NAME,
@@ -2294,6 +2405,21 @@ var MCPClient = class {
2294
2405
  }
2295
2406
  );
2296
2407
  }
2408
+ /** Shared session-shaped data for ensureSession and saveSession */
2409
+ get session() {
2410
+ return {
2411
+ sessionId: this.config.sessionId,
2412
+ userId: this.config.userId,
2413
+ serverId: this.config.serverId,
2414
+ serverName: this.config.serverName,
2415
+ serverUrl: this.config.serverUrl,
2416
+ callbackUrl: this.config.callbackUrl,
2417
+ transportType: this.config.transportType || "streamable-http",
2418
+ headers: this.config.headers,
2419
+ createdAt: this.createdAt ?? Date.now(),
2420
+ updatedAt: Date.now()
2421
+ };
2422
+ }
2297
2423
  /**
2298
2424
  * Emit a connection state change event
2299
2425
  * @private
@@ -2301,13 +2427,13 @@ var MCPClient = class {
2301
2427
  emitStateChange(newState) {
2302
2428
  const previousState = this.currentState;
2303
2429
  this.currentState = newState;
2304
- if (!this.serverId) return;
2430
+ if (!this.config.serverId) return;
2305
2431
  this._onConnectionEvent.fire({
2306
2432
  type: "state_changed",
2307
- sessionId: this.sessionId,
2308
- serverId: this.serverId,
2309
- serverName: this.serverName || this.serverId,
2310
- serverUrl: this.serverUrl || "",
2433
+ sessionId: this.config.sessionId,
2434
+ serverId: this.config.serverId,
2435
+ serverName: this.config.serverName || this.config.serverId,
2436
+ serverUrl: this.config.serverUrl || "",
2311
2437
  createdAt: this.createdAt,
2312
2438
  state: newState,
2313
2439
  previousState,
@@ -2318,8 +2444,8 @@ var MCPClient = class {
2318
2444
  level: "info",
2319
2445
  message: `Connection state: ${previousState} \u2192 ${newState}`,
2320
2446
  displayMessage: `State changed to ${newState}`,
2321
- sessionId: this.sessionId,
2322
- serverId: this.serverId,
2447
+ sessionId: this.config.sessionId,
2448
+ serverId: this.config.serverId,
2323
2449
  payload: { previousState, newState },
2324
2450
  timestamp: Date.now(),
2325
2451
  id: nanoid.nanoid()
@@ -2330,11 +2456,11 @@ var MCPClient = class {
2330
2456
  * @private
2331
2457
  */
2332
2458
  emitError(error, errorType = "unknown") {
2333
- if (!this.serverId) return;
2459
+ if (!this.config.serverId) return;
2334
2460
  this._onConnectionEvent.fire({
2335
2461
  type: "error",
2336
- sessionId: this.sessionId,
2337
- serverId: this.serverId,
2462
+ sessionId: this.config.sessionId,
2463
+ serverId: this.config.serverId,
2338
2464
  error,
2339
2465
  errorType,
2340
2466
  timestamp: Date.now()
@@ -2344,8 +2470,8 @@ var MCPClient = class {
2344
2470
  level: "error",
2345
2471
  message: error,
2346
2472
  displayMessage: error,
2347
- sessionId: this.sessionId,
2348
- serverId: this.serverId,
2473
+ sessionId: this.config.sessionId,
2474
+ serverId: this.config.serverId,
2349
2475
  payload: { errorType, error },
2350
2476
  timestamp: Date.now(),
2351
2477
  id: nanoid.nanoid()
@@ -2356,11 +2482,11 @@ var MCPClient = class {
2356
2482
  * @private
2357
2483
  */
2358
2484
  emitProgress(message) {
2359
- if (!this.serverId) return;
2485
+ if (!this.config.serverId) return;
2360
2486
  this._onConnectionEvent.fire({
2361
2487
  type: "progress",
2362
- sessionId: this.sessionId,
2363
- serverId: this.serverId,
2488
+ sessionId: this.config.sessionId,
2489
+ serverId: this.config.serverId,
2364
2490
  message,
2365
2491
  timestamp: Date.now()
2366
2492
  });
@@ -2378,14 +2504,14 @@ var MCPClient = class {
2378
2504
  * @private
2379
2505
  */
2380
2506
  getTransport(type) {
2381
- if (!this.serverUrl) {
2507
+ if (!this.config.serverUrl) {
2382
2508
  throw new Error("Server URL is required to create transport");
2383
2509
  }
2384
- const baseUrl = new URL(this.serverUrl);
2385
- const hasAuthorizationHeader = Object.keys(this.headers || {}).some((key) => key.toLowerCase() === "authorization");
2510
+ const baseUrl = new URL(this.config.serverUrl);
2511
+ const hasAuthorizationHeader = Object.keys(this.config.headers || {}).some((key) => key.toLowerCase() === "authorization");
2386
2512
  const transportOptions = {
2387
2513
  ...!hasAuthorizationHeader && { authProvider: this.oauthProvider },
2388
- ...this.headers && { requestInit: { headers: this.headers } },
2514
+ ...this.config.headers && { requestInit: { headers: this.config.headers } },
2389
2515
  /**
2390
2516
  * Custom fetch implementation to handle connection timeouts.
2391
2517
  * Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
@@ -2423,78 +2549,74 @@ var MCPClient = class {
2423
2549
  * @private
2424
2550
  */
2425
2551
  async ensureSession() {
2426
- if (this.oauthProvider) {
2427
- return;
2428
- }
2552
+ if (this.oauthProvider) return;
2429
2553
  this.emitStateChange("INITIALIZING");
2430
2554
  this.emitProgress("Loading session configuration...");
2431
- let existingSession = null;
2432
- if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
2433
- existingSession = await sessions.get(this.userId, this.sessionId);
2434
- if (!existingSession) {
2435
- throw new Error(`Session not found: ${this.sessionId}`);
2555
+ if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
2556
+ const existingSession2 = await this._store.get(this.config.userId, this.config.sessionId);
2557
+ if (!existingSession2) {
2558
+ throw new Error(`Session not found: ${this.config.sessionId}`);
2436
2559
  }
2437
- this.serverUrl = this.serverUrl || existingSession.serverUrl;
2438
- this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
2439
- this.serverName = this.serverName || existingSession.serverName;
2440
- this.serverId = this.serverId || existingSession.serverId || "unknown";
2441
- this.headers = this.headers || existingSession.headers;
2442
- this.createdAt = existingSession.createdAt;
2443
- }
2444
- if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
2560
+ this.config.serverUrl = this.config.serverUrl || existingSession2.serverUrl;
2561
+ this.config.callbackUrl = this.config.callbackUrl || existingSession2.callbackUrl;
2562
+ this.config.serverName = this.config.serverName || existingSession2.serverName;
2563
+ this.config.serverId = this.config.serverId || existingSession2.serverId || "unknown";
2564
+ this.config.headers = this.config.headers || existingSession2.headers;
2565
+ this.createdAt = existingSession2.createdAt;
2566
+ }
2567
+ if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
2445
2568
  throw new Error("Missing required connection metadata");
2446
2569
  }
2447
- if (!this.oauthProvider) {
2448
- if (!this.serverId) {
2449
- throw new Error("serverId required for OAuth provider initialization");
2450
- }
2451
- this.oauthProvider = new StorageOAuthClientProvider({
2452
- userId: this.userId,
2453
- serverId: this.serverId,
2454
- sessionId: this.sessionId,
2455
- redirectUrl: this.callbackUrl,
2456
- clientName: this.clientName,
2457
- clientUri: this.clientUri,
2458
- logoUri: this.logoUri,
2459
- policyUri: this.policyUri,
2460
- clientId: this.clientId,
2461
- clientSecret: this.clientSecret,
2462
- onRedirect: (redirectUrl) => {
2463
- if (this.onRedirect) {
2464
- this.onRedirect(redirectUrl);
2465
- }
2570
+ this.oauthProvider = new StorageOAuthClientProvider({
2571
+ userId: this.config.userId,
2572
+ serverId: this.config.serverId,
2573
+ sessionId: this.config.sessionId,
2574
+ redirectUrl: this.config.callbackUrl,
2575
+ clientName: this.config.clientName,
2576
+ clientUri: this.config.clientUri,
2577
+ logoUri: this.config.logoUri,
2578
+ policyUri: this.config.policyUri,
2579
+ clientId: this.config.clientId,
2580
+ clientSecret: this.config.clientSecret,
2581
+ cachedTokens: this.config.cachedCredentials?.tokens,
2582
+ sessionStore: this._store,
2583
+ onRedirect: (redirectUrl) => {
2584
+ if (this.config.serverId) {
2585
+ this._onConnectionEvent.fire({
2586
+ type: "auth_required",
2587
+ sessionId: this.config.sessionId,
2588
+ serverId: this.config.serverId,
2589
+ authUrl: redirectUrl,
2590
+ timestamp: Date.now()
2591
+ });
2466
2592
  }
2467
- });
2468
- }
2469
- if (existingSession === null) {
2470
- existingSession = await sessions.get(this.userId, this.sessionId);
2471
- }
2472
- if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
2593
+ if (this.config.onRedirect) {
2594
+ this.config.onRedirect(redirectUrl);
2595
+ }
2596
+ }
2597
+ });
2598
+ const existingSession = this.config.hasSession ? {} : await this._store.get(this.config.userId, this.config.sessionId);
2599
+ if (!existingSession) {
2473
2600
  this.createdAt = Date.now();
2474
2601
  const updatedAt = this.createdAt;
2475
2602
  this._onObservabilityEvent.fire({
2476
2603
  type: "mcp:client:session_created",
2477
2604
  level: "info",
2478
- message: `Creating pending session ${this.sessionId} for connection setup`,
2479
- sessionId: this.sessionId,
2480
- serverId: this.serverId,
2605
+ message: `Creating pending session ${this.config.sessionId} for connection setup`,
2606
+ sessionId: this.config.sessionId,
2607
+ serverId: this.config.serverId,
2481
2608
  timestamp: Date.now(),
2482
2609
  id: nanoid.nanoid()
2483
2610
  });
2484
- await sessions.create({
2485
- sessionId: this.sessionId,
2486
- userId: this.userId,
2487
- serverId: this.serverId,
2488
- serverName: this.serverName,
2489
- serverUrl: this.serverUrl,
2490
- callbackUrl: this.callbackUrl,
2491
- transportType: this.transportType || "streamable-http",
2492
- headers: this.headers,
2493
- createdAt: this.createdAt,
2611
+ await this._store.create({
2612
+ ...this.session,
2494
2613
  updatedAt,
2495
2614
  status: "pending"
2496
2615
  });
2497
2616
  }
2617
+ if (!existingSession && this.oauthProvider instanceof StorageOAuthClientProvider) {
2618
+ await this.oauthProvider.initializeCredentials();
2619
+ }
2498
2620
  }
2499
2621
  /**
2500
2622
  * Saves current session state to the session store
@@ -2502,34 +2624,14 @@ var MCPClient = class {
2502
2624
  * @param status - Session lifecycle status used by storage cleanup
2503
2625
  * @private
2504
2626
  */
2505
- async saveSession(status = "active", existingSession) {
2506
- if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
2627
+ async saveSession(status = "active") {
2628
+ if (!this.config.sessionId || !this.config.serverId || !this.config.serverUrl || !this.config.callbackUrl) {
2507
2629
  return;
2508
2630
  }
2509
- const sessionData = {
2510
- sessionId: this.sessionId,
2511
- userId: this.userId,
2512
- serverId: this.serverId,
2513
- serverName: this.serverName,
2514
- serverUrl: this.serverUrl,
2515
- callbackUrl: this.callbackUrl,
2516
- transportType: this.transportType || "streamable-http",
2517
- headers: this.headers,
2518
- createdAt: this.createdAt || Date.now(),
2519
- updatedAt: Date.now(),
2631
+ await this._store.update(this.config.userId, this.config.sessionId, {
2632
+ ...this.session,
2520
2633
  status
2521
- };
2522
- if (status === "active") {
2523
- sessionData.authUrl = null;
2524
- }
2525
- if (existingSession === void 0) {
2526
- existingSession = await sessions.get(this.userId, this.sessionId);
2527
- }
2528
- if (existingSession) {
2529
- await sessions.update(this.userId, this.sessionId, sessionData);
2530
- } else {
2531
- await sessions.create(sessionData);
2532
- }
2634
+ });
2533
2635
  }
2534
2636
  /**
2535
2637
  * Removes transient setup/auth sessions without masking the original error.
@@ -2537,7 +2639,7 @@ var MCPClient = class {
2537
2639
  */
2538
2640
  async deleteTransientSession() {
2539
2641
  try {
2540
- await sessions.delete(this.userId, this.sessionId);
2642
+ await this._store.delete(this.config.userId, this.config.sessionId);
2541
2643
  } catch {
2542
2644
  }
2543
2645
  }
@@ -2547,7 +2649,7 @@ var MCPClient = class {
2547
2649
  * @private
2548
2650
  */
2549
2651
  async tryConnect() {
2550
- const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
2652
+ const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
2551
2653
  let lastError;
2552
2654
  for (const currentType of transportsToTry) {
2553
2655
  const isLastAttempt = currentType === transportsToTry[transportsToTry.length - 1];
@@ -2555,6 +2657,7 @@ var MCPClient = class {
2555
2657
  const transport = this.getTransport(currentType);
2556
2658
  this.transport = transport;
2557
2659
  await this.client.connect(transport);
2660
+ this._serverInfo = this.client.getServerVersion();
2558
2661
  return { transportType: currentType };
2559
2662
  } catch (error) {
2560
2663
  lastError = error;
@@ -2570,8 +2673,8 @@ var MCPClient = class {
2570
2673
  this._onObservabilityEvent.fire({
2571
2674
  level: "warn",
2572
2675
  message: `Transport ${currentType} failed, falling back`,
2573
- sessionId: this.sessionId,
2574
- serverId: this.serverId,
2676
+ sessionId: this.config.sessionId,
2677
+ serverId: this.config.serverId,
2575
2678
  metadata: {
2576
2679
  failedTransport: currentType,
2577
2680
  error: errorMessage
@@ -2614,15 +2717,15 @@ var MCPClient = class {
2614
2717
  try {
2615
2718
  this.emitStateChange("CONNECTING");
2616
2719
  const { transportType } = await this.tryConnect();
2617
- this.transportType = transportType;
2720
+ this.config.transportType = transportType;
2618
2721
  this.emitStateChange("CONNECTED");
2619
2722
  this.emitProgress("Connected successfully");
2620
2723
  this._onObservabilityEvent.fire({
2621
2724
  type: "mcp:client:session_saved",
2622
2725
  level: "info",
2623
- message: `Saving active session ${this.sessionId} (connect success)`,
2624
- sessionId: this.sessionId,
2625
- serverId: this.serverId,
2726
+ message: `Saving active session ${this.config.sessionId} (connect success)`,
2727
+ sessionId: this.config.sessionId,
2728
+ serverId: this.config.serverId,
2626
2729
  timestamp: Date.now(),
2627
2730
  id: nanoid.nanoid()
2628
2731
  });
@@ -2645,23 +2748,23 @@ var MCPClient = class {
2645
2748
  this._onObservabilityEvent.fire({
2646
2749
  type: "mcp:client:session_saved",
2647
2750
  level: "info",
2648
- message: `Saving pending OAuth session ${this.sessionId}`,
2649
- sessionId: this.sessionId,
2650
- serverId: this.serverId,
2751
+ message: `Saving pending OAuth session ${this.config.sessionId}`,
2752
+ sessionId: this.config.sessionId,
2753
+ serverId: this.config.serverId,
2651
2754
  timestamp: Date.now(),
2652
2755
  id: nanoid.nanoid()
2653
2756
  });
2654
2757
  await this.saveSession("pending");
2655
- if (this.serverId) {
2758
+ if (this.config.serverId) {
2656
2759
  this._onConnectionEvent.fire({
2657
2760
  type: "auth_required",
2658
- sessionId: this.sessionId,
2659
- serverId: this.serverId,
2761
+ sessionId: this.config.sessionId,
2762
+ serverId: this.config.serverId,
2660
2763
  authUrl,
2661
2764
  timestamp: Date.now()
2662
2765
  });
2663
- if (authUrl && this.onRedirect) {
2664
- this.onRedirect(authUrl);
2766
+ if (authUrl && this.config.onRedirect) {
2767
+ this.config.onRedirect(authUrl);
2665
2768
  }
2666
2769
  }
2667
2770
  throw new UnauthorizedError("OAuth authorization required");
@@ -2670,9 +2773,9 @@ var MCPClient = class {
2670
2773
  this.emitError(errorMessage, "connection");
2671
2774
  this.emitStateChange("FAILED");
2672
2775
  try {
2673
- const existingSession = await sessions.get(this.userId, this.sessionId);
2776
+ const existingSession = await this._store.get(this.config.userId, this.config.sessionId);
2674
2777
  if (!existingSession || existingSession.status !== "active") {
2675
- await sessions.delete(this.userId, this.sessionId);
2778
+ await this._store.delete(this.config.userId, this.config.sessionId);
2676
2779
  }
2677
2780
  } catch {
2678
2781
  }
@@ -2697,16 +2800,16 @@ var MCPClient = class {
2697
2800
  throw new Error(error);
2698
2801
  }
2699
2802
  if (state) {
2700
- const stateCheck = await this.oauthProvider.checkState(state);
2701
- if (!stateCheck.valid) {
2702
- const error = stateCheck.error || "Invalid OAuth state";
2703
- this.emitError(error, "auth");
2803
+ try {
2804
+ await this.oauthProvider.consumeState(state);
2805
+ } catch (error) {
2806
+ const msg = error instanceof Error ? error.message : "Invalid OAuth state";
2807
+ this.emitError(msg, "auth");
2704
2808
  this.emitStateChange("FAILED");
2705
- throw new Error(error);
2809
+ throw error;
2706
2810
  }
2707
- await this.oauthProvider.consumeState(state);
2708
2811
  }
2709
- const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
2812
+ const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
2710
2813
  let lastError;
2711
2814
  let tokensExchanged = false;
2712
2815
  let authenticatedStateEmitted = false;
@@ -2733,14 +2836,15 @@ var MCPClient = class {
2733
2836
  }
2734
2837
  }
2735
2838
  await this.client.connect(this.transport);
2736
- this.transportType = currentType;
2839
+ this._serverInfo = this.client.getServerVersion();
2840
+ this.config.transportType = currentType;
2737
2841
  this.emitStateChange("CONNECTED");
2738
2842
  this._onObservabilityEvent.fire({
2739
2843
  type: "mcp:client:session_saved",
2740
2844
  level: "info",
2741
- message: `Saving active session ${this.sessionId} (OAuth complete)`,
2742
- sessionId: this.sessionId,
2743
- serverId: this.serverId,
2845
+ message: `Saving active session ${this.config.sessionId} (OAuth complete)`,
2846
+ sessionId: this.config.sessionId,
2847
+ serverId: this.config.serverId,
2744
2848
  timestamp: Date.now(),
2745
2849
  id: nanoid.nanoid()
2746
2850
  });
@@ -2813,11 +2917,11 @@ var MCPClient = class {
2813
2917
  this.emitStateChange("DISCOVERING");
2814
2918
  try {
2815
2919
  const result = await this.fetchTools();
2816
- if (this.serverId) {
2920
+ if (this.config.serverId) {
2817
2921
  this._onConnectionEvent.fire({
2818
2922
  type: "tools_discovered",
2819
- sessionId: this.sessionId,
2820
- serverId: this.serverId,
2923
+ sessionId: this.config.sessionId,
2924
+ serverId: this.config.serverId,
2821
2925
  toolCount: result.tools.length,
2822
2926
  tools: result.tools,
2823
2927
  timestamp: Date.now()
@@ -2857,8 +2961,8 @@ var MCPClient = class {
2857
2961
  level: "info",
2858
2962
  message: `Tool ${toolName} called successfully`,
2859
2963
  displayMessage: `Called tool ${toolName}`,
2860
- sessionId: this.sessionId,
2861
- serverId: this.serverId,
2964
+ sessionId: this.config.sessionId,
2965
+ serverId: this.config.serverId,
2862
2966
  payload: {
2863
2967
  toolName,
2864
2968
  args: toolArgs
@@ -2874,8 +2978,8 @@ var MCPClient = class {
2874
2978
  level: "error",
2875
2979
  message: errorMessage,
2876
2980
  displayMessage: `Failed to call tool ${toolName}`,
2877
- sessionId: this.sessionId,
2878
- serverId: this.serverId,
2981
+ sessionId: this.config.sessionId,
2982
+ serverId: this.config.serverId,
2879
2983
  payload: {
2880
2984
  errorType: "tool_execution",
2881
2985
  error: errorMessage,
@@ -3023,8 +3127,8 @@ var MCPClient = class {
3023
3127
  type: "mcp:client:error",
3024
3128
  level: "warn",
3025
3129
  message: "Initialization failed during clearSession",
3026
- sessionId: this.sessionId,
3027
- serverId: this.serverId,
3130
+ sessionId: this.config.sessionId,
3131
+ serverId: this.config.serverId,
3028
3132
  payload: { error: String(error) },
3029
3133
  timestamp: Date.now(),
3030
3134
  id: nanoid.nanoid()
@@ -3033,7 +3137,7 @@ var MCPClient = class {
3033
3137
  if (this.oauthProvider) {
3034
3138
  await this.oauthProvider.invalidateCredentials("all");
3035
3139
  }
3036
- await sessions.delete(this.userId, this.sessionId);
3140
+ await this._store.delete(this.config.userId, this.config.sessionId);
3037
3141
  await this.disconnect();
3038
3142
  }
3039
3143
  /**
@@ -3067,19 +3171,19 @@ var MCPClient = class {
3067
3171
  }
3068
3172
  this.oauthProvider = null;
3069
3173
  this.transport = null;
3070
- if (this.serverId) {
3174
+ if (this.config.serverId) {
3071
3175
  this._onConnectionEvent.fire({
3072
3176
  type: "disconnected",
3073
- sessionId: this.sessionId,
3074
- serverId: this.serverId,
3177
+ sessionId: this.config.sessionId,
3178
+ serverId: this.config.serverId,
3075
3179
  timestamp: Date.now()
3076
3180
  });
3077
3181
  this._onObservabilityEvent.fire({
3078
3182
  type: "mcp:client:disconnect",
3079
3183
  level: "info",
3080
- message: `Disconnected from ${this.serverId}`,
3081
- sessionId: this.sessionId,
3082
- serverId: this.serverId,
3184
+ message: `Disconnected from ${this.config.serverId}`,
3185
+ sessionId: this.config.sessionId,
3186
+ serverId: this.config.serverId,
3083
3187
  payload: {},
3084
3188
  timestamp: Date.now(),
3085
3189
  id: nanoid.nanoid()
@@ -3104,42 +3208,52 @@ var MCPClient = class {
3104
3208
  * @returns Server URL or empty string if not set
3105
3209
  */
3106
3210
  getServerUrl() {
3107
- return this.serverUrl || "";
3211
+ return this.config.serverUrl || "";
3108
3212
  }
3109
3213
  /**
3110
3214
  * Gets the OAuth callback URL
3111
3215
  * @returns Callback URL or empty string if not set
3112
3216
  */
3113
3217
  getCallbackUrl() {
3114
- return this.callbackUrl || "";
3218
+ return this.config.callbackUrl || "";
3115
3219
  }
3116
3220
  /**
3117
3221
  * Gets the transport type being used
3118
3222
  * @returns Transport type (defaults to 'streamable-http')
3119
3223
  */
3120
3224
  getTransportType() {
3121
- return this.transportType || "streamable-http";
3225
+ return this.config.transportType || "streamable-http";
3226
+ }
3227
+ /**
3228
+ * Gets the full server metadata from the MCP initialize response.
3229
+ * Includes name, version, icons, title, description, and website URL.
3230
+ * Returns undefined if the client hasn't connected yet.
3231
+ */
3232
+ getServerInfo() {
3233
+ return this._serverInfo;
3122
3234
  }
3123
3235
  /**
3124
- * Gets the human-readable server name
3236
+ * Gets the human-readable server name.
3237
+ * Prefers the server's reported title/name from the initialize response,
3238
+ * falling back to the name provided at construction or session metadata.
3125
3239
  * @returns Server name or undefined
3126
3240
  */
3127
3241
  getServerName() {
3128
- return this.serverName;
3242
+ return this.config.serverName;
3129
3243
  }
3130
3244
  /**
3131
3245
  * Gets the server ID
3132
3246
  * @returns Server ID or undefined
3133
3247
  */
3134
3248
  getServerId() {
3135
- return this.serverId;
3249
+ return this.config.serverId;
3136
3250
  }
3137
3251
  /**
3138
3252
  * Gets the session ID
3139
3253
  * @returns Session ID
3140
3254
  */
3141
3255
  getSessionId() {
3142
- return this.sessionId;
3256
+ return this.config.sessionId;
3143
3257
  }
3144
3258
  };
3145
3259
 
@@ -3166,6 +3280,14 @@ var ToolPolicyGateway = class {
3166
3280
  getServerId() {
3167
3281
  return this.client.getServerId?.();
3168
3282
  }
3283
+ /**
3284
+ * Returns the full server metadata from the underlying client, if available.
3285
+ * Includes name, version, icons, title, description, and website URL.
3286
+ * Available only after the client has connected and completed initialization.
3287
+ */
3288
+ getServerInfo() {
3289
+ return this.client.getServerInfo?.();
3290
+ }
3169
3291
  /**
3170
3292
  * Returns the human-readable server name from the underlying client, if available.
3171
3293
  */
@@ -3294,6 +3416,7 @@ var MultiSessionClient = class {
3294
3416
  __publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
3295
3417
  __publicField(this, "userId");
3296
3418
  __publicField(this, "options");
3419
+ __publicField(this, "_store");
3297
3420
  this.userId = userId;
3298
3421
  this.options = {
3299
3422
  timeout: DEFAULT_TIMEOUT_MS,
@@ -3301,6 +3424,7 @@ var MultiSessionClient = class {
3301
3424
  retryDelay: DEFAULT_RETRY_DELAY_MS,
3302
3425
  ...options
3303
3426
  };
3427
+ this._store = options.onObservabilityEvent ? withDbObservability(options.sessionStore ?? sessions, options.onObservabilityEvent) : options.sessionStore ?? sessions;
3304
3428
  }
3305
3429
  // -----------------------------------------------------------------------
3306
3430
  // Public API
@@ -3384,9 +3508,9 @@ var MultiSessionClient = class {
3384
3508
  * to querying the storage backend via `sessions.list(userId)`.
3385
3509
  */
3386
3510
  async fetchActiveSessions() {
3387
- const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await sessions.list(this.userId);
3511
+ const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await this._store.list(this.userId);
3388
3512
  return sessionList.filter(
3389
- (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
3513
+ (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active" && s.enabled !== false
3390
3514
  );
3391
3515
  }
3392
3516
  /**
@@ -3459,8 +3583,13 @@ var MultiSessionClient = class {
3459
3583
  callbackUrl: session.callbackUrl,
3460
3584
  serverName: session.serverName,
3461
3585
  transportType: session.transportType,
3462
- headers: session.headers
3586
+ headers: session.headers,
3587
+ hasSession: true,
3588
+ sessionStore: this._store
3463
3589
  });
3590
+ if (this.options.onObservabilityEvent) {
3591
+ client.onObservabilityEvent(this.options.onObservabilityEvent);
3592
+ }
3464
3593
  const timeoutMs = this.options.timeout;
3465
3594
  let timeoutTimer;
3466
3595
  const timeoutPromise = new Promise((_, reject) => {
@@ -3519,130 +3648,188 @@ function isConnectionEvent(event) {
3519
3648
  }
3520
3649
 
3521
3650
  // src/server/handlers/sse-handler.ts
3522
- var DEFAULT_HEARTBEAT_INTERVAL = 3e4;
3651
+ var DEFAULT_HEARTBEAT_MS = 3e4;
3523
3652
  function normalizeHeaders(headers) {
3524
3653
  if (!headers || typeof headers !== "object") return void 0;
3525
- const entries = Object.entries(headers).map(([key, value]) => [key.trim(), String(value).trim()]).filter(([key, value]) => key.length > 0 && value.length > 0);
3654
+ const entries = Object.entries(headers).map(([k, v]) => [k.trim(), String(v).trim()]).filter(([k, v]) => k.length > 0 && v.length > 0);
3526
3655
  return entries.length > 0 ? Object.fromEntries(entries) : void 0;
3527
3656
  }
3657
+ function validatingStateEvent(sessionId, session) {
3658
+ return {
3659
+ type: "state_changed",
3660
+ sessionId,
3661
+ serverId: session.serverId ?? "unknown",
3662
+ serverName: session.serverName ?? "Unknown",
3663
+ serverUrl: session.serverUrl,
3664
+ state: "VALIDATING",
3665
+ previousState: "DISCONNECTED",
3666
+ timestamp: Date.now()
3667
+ };
3668
+ }
3669
+ function connectionErrorEvent(sessionId, serverId, error, errorType) {
3670
+ return {
3671
+ type: "error",
3672
+ sessionId,
3673
+ serverId: serverId ?? "unknown",
3674
+ error: error instanceof Error ? error.message : "Connection failed",
3675
+ errorType,
3676
+ timestamp: Date.now()
3677
+ };
3678
+ }
3528
3679
  var SSEConnectionManager = class {
3680
+ // -----------------------------------------------------------------------
3681
+ // Constructor
3682
+ // -----------------------------------------------------------------------
3683
+ /**
3684
+ * @param options - Handler configuration (userId, auth, metadata, heartbeat, observability).
3685
+ * @param sendEvent - Callback that writes a typed event onto the SSE stream.
3686
+ */
3529
3687
  constructor(options, sendEvent) {
3530
3688
  this.options = options;
3531
3689
  this.sendEvent = sendEvent;
3690
+ // -----------------------------------------------------------------------
3691
+ // State
3692
+ // -----------------------------------------------------------------------
3532
3693
  __publicField(this, "userId");
3694
+ /** Active MCP transports keyed by sessionId. */
3533
3695
  __publicField(this, "clients", /* @__PURE__ */ new Map());
3696
+ /** Instrumented session store — always wraps `sessions` with DB observability. */
3697
+ __publicField(this, "observedStore");
3534
3698
  __publicField(this, "heartbeatTimer");
3535
- __publicField(this, "isActive", true);
3699
+ __publicField(this, "active", true);
3536
3700
  this.userId = options.userId;
3701
+ this.observedStore = withDbObservability(sessions, (event) => this.sendEvent(event));
3537
3702
  this.startHeartbeat();
3538
3703
  }
3704
+ // -----------------------------------------------------------------------
3705
+ // Public API
3706
+ // -----------------------------------------------------------------------
3539
3707
  /**
3540
- * Get resolved client metadata (dynamic > static > defaults)
3541
- */
3542
- async getResolvedClientMetadata(request) {
3543
- let metadata = {};
3544
- if (this.options.clientDefaults) {
3545
- metadata = { ...this.options.clientDefaults };
3546
- }
3547
- if (this.options.getClientMetadata) {
3548
- const dynamicMetadata = await this.options.getClientMetadata(request);
3549
- metadata = { ...metadata, ...dynamicMetadata };
3550
- }
3551
- return metadata;
3552
- }
3553
- /**
3554
- * Start heartbeat to keep connection alive
3555
- */
3556
- startHeartbeat() {
3557
- const interval = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL;
3558
- this.heartbeatTimer = setInterval(() => {
3559
- if (this.isActive) {
3560
- this.sendEvent({
3561
- level: "debug",
3562
- message: "heartbeat",
3563
- timestamp: Date.now()
3564
- });
3565
- }
3566
- }, interval);
3567
- }
3568
- /**
3569
- * Handle incoming RPC requests
3570
- * Returns the RPC response directly for immediate HTTP response (bypassing SSE latency)
3708
+ * Dispatches an incoming RPC request, emits timing observability,
3709
+ * and returns the response.
3710
+ *
3711
+ * Emits `rpc:start` before dispatch and `rpc:end` on completion
3712
+ * (success or error), each carrying the method name, sessionId, and
3713
+ * duration. All events flow through the unified `onObservability`
3714
+ * and the SSE stream.
3715
+ *
3716
+ * @param request - The deserialized RPC envelope.
3717
+ * @returns The RPC response (success or error).
3571
3718
  */
3572
3719
  async handleRequest(request) {
3720
+ const method = request.method;
3721
+ const sessionId = request.params?.sessionId;
3722
+ const t0 = performance.now();
3723
+ this.sendEvent({
3724
+ type: "rpc:start",
3725
+ level: "debug",
3726
+ message: method,
3727
+ sessionId,
3728
+ timestamp: Date.now()
3729
+ });
3573
3730
  try {
3574
- let result;
3575
- switch (request.method) {
3576
- case "listSessions":
3577
- result = await this.listSessions();
3578
- break;
3579
- case "connect":
3580
- result = await this.connect(request.params);
3581
- break;
3582
- case "reconnect":
3583
- result = await this.reconnect(request.params);
3584
- break;
3585
- case "disconnect":
3586
- result = await this.disconnect(request.params);
3587
- break;
3588
- case "listTools":
3589
- result = await this.listTools(request.params);
3590
- break;
3591
- case "setToolPolicy":
3592
- result = await this.setToolPolicy(request.params);
3593
- break;
3594
- case "getToolPolicy":
3595
- result = await this.getToolPolicy(request.params);
3596
- break;
3597
- case "callTool":
3598
- result = await this.callTool(request.params);
3599
- break;
3600
- case "getSession":
3601
- result = await this.getSession(request.params);
3602
- break;
3603
- case "finishAuth":
3604
- result = await this.finishAuth(request.params);
3605
- break;
3606
- case "listPrompts":
3607
- result = await this.listPrompts(request.params);
3608
- break;
3609
- case "getPrompt":
3610
- result = await this.getPrompt(request.params);
3611
- break;
3612
- case "listResources":
3613
- result = await this.listResources(request.params);
3614
- break;
3615
- case "readResource":
3616
- result = await this.readResource(request.params);
3617
- break;
3618
- default:
3619
- throw new Error(`Unknown method: ${request.method}`);
3620
- }
3621
- const response = {
3622
- id: request.id,
3623
- result
3624
- };
3731
+ const result = await this.dispatchImpl(request);
3732
+ this.sendEvent({
3733
+ type: "rpc:end",
3734
+ level: "debug",
3735
+ message: method,
3736
+ sessionId,
3737
+ payload: { durationMs: performance.now() - t0 },
3738
+ timestamp: Date.now()
3739
+ });
3740
+ const response = { id: request.id, result };
3625
3741
  this.sendEvent(response);
3626
3742
  return response;
3627
3743
  } catch (error) {
3628
- const errorResponse = {
3744
+ this.sendEvent({
3745
+ type: "rpc:end",
3746
+ level: "error",
3747
+ message: method,
3748
+ sessionId,
3749
+ payload: { durationMs: performance.now() - t0, error: String(error) },
3750
+ timestamp: Date.now()
3751
+ });
3752
+ const response = {
3629
3753
  id: request.id,
3630
3754
  error: {
3631
3755
  code: RpcErrorCodes.EXECUTION_ERROR,
3632
3756
  message: error instanceof Error ? error.message : "Unknown error"
3633
3757
  }
3634
3758
  };
3635
- this.sendEvent(errorResponse);
3636
- return errorResponse;
3759
+ this.sendEvent(response);
3760
+ return response;
3761
+ }
3762
+ }
3763
+ /**
3764
+ * Tears down all active MCP transports and stops the heartbeat timer.
3765
+ *
3766
+ * Disconnects are issued in parallel across all sessions. After calling
3767
+ * this method the manager instance should be discarded.
3768
+ */
3769
+ async dispose() {
3770
+ this.active = false;
3771
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
3772
+ await Promise.all(
3773
+ Array.from(this.clients.values()).map((c) => c.disconnect())
3774
+ );
3775
+ this.clients.clear();
3776
+ }
3777
+ // -----------------------------------------------------------------------
3778
+ // RPC Dispatch (raw — called by handleRequest which adds timing)
3779
+ // -----------------------------------------------------------------------
3780
+ /**
3781
+ * Routes an RPC method name to the appropriate private handler.
3782
+ *
3783
+ * @throws {Error} When the method name is unrecognized.
3784
+ */
3785
+ async dispatchImpl(request) {
3786
+ switch (request.method) {
3787
+ case "listSessions":
3788
+ return this.listSessions();
3789
+ case "connect":
3790
+ return this.connect(request.params);
3791
+ case "reconnect":
3792
+ return this.reconnect(request.params);
3793
+ case "disconnect":
3794
+ return this.disconnect(request.params);
3795
+ case "listTools":
3796
+ return this.listTools(request.params);
3797
+ case "setToolPolicy":
3798
+ return this.setToolPolicy(request.params);
3799
+ case "getToolPolicy":
3800
+ return this.getToolPolicy(request.params);
3801
+ case "updateSession":
3802
+ return this.updateSession(request.params);
3803
+ case "callTool":
3804
+ return this.callTool(request.params);
3805
+ case "getSession":
3806
+ return this.getSession(request.params);
3807
+ case "finishAuth":
3808
+ return this.finishAuth(request.params);
3809
+ case "listPrompts":
3810
+ return this.listPrompts(request.params);
3811
+ case "getPrompt":
3812
+ return this.getPrompt(request.params);
3813
+ case "listResources":
3814
+ return this.listResources(request.params);
3815
+ case "readResource":
3816
+ return this.readResource(request.params);
3817
+ default:
3818
+ throw new Error(`Unknown RPC method: ${request.method}`);
3637
3819
  }
3638
3820
  }
3821
+ // -----------------------------------------------------------------------
3822
+ // Session Query
3823
+ // -----------------------------------------------------------------------
3639
3824
  /**
3640
- * Get all sessions for the current userId
3825
+ * Lists all sessions owned by the current user.
3826
+ *
3827
+ * Returns a lightweight view — session metadata only, no credential fields.
3641
3828
  */
3642
3829
  async listSessions() {
3643
- const sessionList = await sessions.list(this.userId);
3830
+ const all = await sessions.list(this.userId);
3644
3831
  return {
3645
- sessions: sessionList.map((s) => ({
3832
+ sessions: all.map((s) => ({
3646
3833
  sessionId: s.sessionId,
3647
3834
  serverId: s.serverId,
3648
3835
  serverName: s.serverName,
@@ -3651,215 +3838,203 @@ var SSEConnectionManager = class {
3651
3838
  createdAt: s.createdAt,
3652
3839
  updatedAt: s.updatedAt ?? s.createdAt,
3653
3840
  status: s.status ?? "pending",
3654
- toolPolicy: s.toolPolicy
3841
+ toolPolicy: s.toolPolicy,
3842
+ enabled: s.enabled ?? true
3655
3843
  }))
3656
3844
  };
3657
3845
  }
3658
3846
  /**
3659
- * Connect to an MCP server
3847
+ * Restores and validates a previously persisted session.
3848
+ *
3849
+ * Loads the full session row (including credentials) from storage, creates
3850
+ * a new MCP transport, connects to the remote server, and emits the
3851
+ * discovered (policy-filtered) tool list via SSE.
3660
3852
  */
3661
- async connect(params) {
3662
- const { serverName, serverUrl, callbackUrl, transportType, clientId, clientSecret } = params;
3663
- const headers = normalizeHeaders(params.headers);
3664
- const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : generateServerId();
3665
- const existingSessions = await sessions.list(this.userId);
3666
- const duplicate = existingSessions.find(
3667
- (s) => s.serverId === serverId || s.serverUrl === serverUrl
3668
- );
3669
- if (duplicate) {
3670
- if (duplicate.status === "pending") {
3671
- await this.getSession({ sessionId: duplicate.sessionId });
3672
- return {
3673
- sessionId: duplicate.sessionId,
3674
- success: true
3675
- };
3676
- }
3677
- throw new Error(`Connection already exists for server: ${duplicate.serverUrl || duplicate.serverId} (${duplicate.serverName})`);
3678
- }
3679
- const sessionId = await sessions.generateSessionId();
3853
+ async getSession(params) {
3854
+ const session = await this.requireSession(params.sessionId);
3855
+ this.sendEvent(validatingStateEvent(params.sessionId, session));
3680
3856
  try {
3681
- const clientMetadata = await this.getResolvedClientMetadata();
3682
- const client = new MCPClient({
3683
- userId: this.userId,
3684
- sessionId,
3685
- serverId,
3686
- serverName,
3687
- serverUrl,
3688
- callbackUrl,
3689
- transportType,
3690
- headers,
3691
- clientId,
3692
- clientSecret,
3693
- ...clientMetadata
3694
- // Spread client metadata (clientName, clientUri, logoUri, policyUri)
3695
- });
3696
- this.clients.set(sessionId, client);
3697
- client.onConnectionEvent((event) => {
3698
- this.emitConnectionEvent(event);
3699
- });
3700
- client.onObservabilityEvent((event) => {
3701
- this.sendEvent(event);
3702
- });
3857
+ const client = this.restoreClient(session);
3858
+ this.attachClientEvents(client);
3703
3859
  await client.connect();
3704
- await this.listPolicyFilteredTools(sessionId);
3705
- return {
3706
- sessionId,
3707
- success: true
3708
- };
3860
+ this.clients.set(params.sessionId, client);
3861
+ const { result } = await this.listPolicyFilteredTools(params.sessionId);
3862
+ return { success: true, toolCount: result.tools.length };
3709
3863
  } catch (error) {
3710
- if (error instanceof UnauthorizedError) {
3711
- this.clients.delete(sessionId);
3712
- return {
3713
- sessionId,
3714
- success: true
3715
- };
3716
- }
3717
- this.emitConnectionEvent({
3718
- type: "error",
3719
- sessionId,
3720
- serverId,
3721
- error: error instanceof Error ? error.message : "Connection failed",
3722
- errorType: "connection",
3723
- timestamp: Date.now()
3724
- });
3725
- this.clients.delete(sessionId);
3864
+ this.sendEvent(connectionErrorEvent(params.sessionId, session.serverId, error, "validation"));
3726
3865
  throw error;
3727
3866
  }
3728
3867
  }
3868
+ // -----------------------------------------------------------------------
3869
+ // Connection Lifecycle
3870
+ // -----------------------------------------------------------------------
3729
3871
  /**
3730
- * Reconnect to an MCP server — tears down the active client transport/connection
3731
- * and creates a fresh connection while reusing the existing session credentials in a single RPC call.
3872
+ * Initiates a connection to a new MCP server.
3873
+ *
3874
+ * If a session for the same `serverId` or `serverUrl` already exists and
3875
+ * is still in a `pending` (OAuth) state, the existing session is resumed
3876
+ * instead of creating a duplicate.
3877
+ *
3878
+ * `UnauthorizedError` is treated as a pending-auth state and returned as
3879
+ * a successful result (the client will then redirect to the auth URL).
3732
3880
  */
3733
- async reconnect(params) {
3734
- const { serverId: rawServerId, serverName, serverUrl, callbackUrl, transportType, clientId, clientSecret } = params;
3881
+ async connect(params) {
3735
3882
  const headers = normalizeHeaders(params.headers);
3736
- const serverId = rawServerId && rawServerId.length <= 12 ? rawServerId : generateServerId();
3737
- const existingSessions = await sessions.list(this.userId);
3738
- const duplicate = existingSessions.find(
3739
- (s) => s.serverId === serverId || s.serverUrl === serverUrl
3740
- );
3741
- const sessionId = duplicate ? duplicate.sessionId : await sessions.generateSessionId();
3742
- if (duplicate) {
3743
- const existingClient = this.clients.get(duplicate.sessionId);
3744
- if (existingClient) {
3745
- await existingClient.disconnect();
3746
- this.clients.delete(duplicate.sessionId);
3883
+ const serverId = this.normalizeServerId(params.serverId);
3884
+ const existing = await this.findExistingSession(serverId, params.serverUrl);
3885
+ if (existing) {
3886
+ if (existing.status === "pending") {
3887
+ return this.getSession({ sessionId: existing.sessionId }).then(() => ({
3888
+ sessionId: existing.sessionId,
3889
+ success: true
3890
+ }));
3747
3891
  }
3892
+ throw new Error(
3893
+ `Connection already exists for server: ${existing.serverUrl ?? existing.serverId} (${existing.serverName})`
3894
+ );
3748
3895
  }
3749
- try {
3750
- const clientMetadata = await this.getResolvedClientMetadata();
3751
- const client = new MCPClient({
3752
- userId: this.userId,
3753
- sessionId,
3754
- serverId,
3755
- serverName,
3756
- serverUrl,
3757
- callbackUrl,
3758
- transportType,
3759
- headers,
3760
- clientId,
3761
- clientSecret,
3762
- ...clientMetadata
3763
- });
3764
- this.clients.set(sessionId, client);
3765
- client.onConnectionEvent((event) => {
3766
- this.emitConnectionEvent(event);
3767
- });
3768
- client.onObservabilityEvent((event) => {
3769
- this.sendEvent(event);
3770
- });
3771
- await client.connect();
3772
- await this.listPolicyFilteredTools(sessionId);
3773
- return { sessionId, success: true };
3774
- } catch (error) {
3775
- if (error instanceof UnauthorizedError) {
3776
- this.clients.delete(sessionId);
3777
- return { sessionId, success: true };
3896
+ const sessionId = await sessions.generateSessionId();
3897
+ const metadata = await this.getResolvedClientMetadata();
3898
+ const client = new MCPClient({
3899
+ userId: this.userId,
3900
+ sessionId,
3901
+ serverId,
3902
+ serverName: params.serverName,
3903
+ serverUrl: params.serverUrl,
3904
+ callbackUrl: params.callbackUrl,
3905
+ transportType: params.transportType,
3906
+ headers,
3907
+ sessionStore: this.observedStore,
3908
+ ...metadata
3909
+ });
3910
+ this.cacheClient(sessionId, client);
3911
+ return this.connectAndDiscover(client, sessionId, serverId);
3912
+ }
3913
+ /**
3914
+ * Reconnects to an MCP server by tearing down the active transport if one
3915
+ * exists and instantiating a fresh connection, reusing the existing session
3916
+ * (and its stored credentials / DCR client info) from the database.
3917
+ */
3918
+ async reconnect(params) {
3919
+ const headers = normalizeHeaders(params.headers);
3920
+ const serverId = this.normalizeServerId(params.serverId);
3921
+ const existing = await this.findExistingSession(serverId, params.serverUrl);
3922
+ const sessionId = existing ? existing.sessionId : await sessions.generateSessionId();
3923
+ if (existing) {
3924
+ const staleClient = this.clients.get(existing.sessionId);
3925
+ if (staleClient) {
3926
+ await staleClient.disconnect();
3927
+ this.clients.delete(existing.sessionId);
3778
3928
  }
3779
- this.emitConnectionEvent({
3780
- type: "error",
3781
- sessionId,
3782
- serverId,
3783
- error: error instanceof Error ? error.message : "Connection failed",
3784
- errorType: "connection",
3785
- timestamp: Date.now()
3786
- });
3787
- this.clients.delete(sessionId);
3788
- throw error;
3789
3929
  }
3930
+ const metadata = await this.getResolvedClientMetadata();
3931
+ const client = new MCPClient({
3932
+ userId: this.userId,
3933
+ sessionId,
3934
+ serverId,
3935
+ serverName: params.serverName,
3936
+ serverUrl: params.serverUrl,
3937
+ callbackUrl: params.callbackUrl,
3938
+ transportType: params.transportType,
3939
+ headers,
3940
+ sessionStore: this.observedStore,
3941
+ ...metadata
3942
+ });
3943
+ this.cacheClient(sessionId, client);
3944
+ return this.connectAndDiscover(client, sessionId, serverId);
3790
3945
  }
3791
3946
  /**
3792
- * Disconnect from an MCP server
3947
+ * Disconnects from an MCP server.
3948
+ *
3949
+ * If an active in-memory transport exists it delegates to `clearSession()`
3950
+ * (which sends the server an HTTP DELETE per the Streamable spec). If no
3951
+ * active transport is available (orphaned session from a failed OAuth flow),
3952
+ * the session row is removed directly from storage.
3793
3953
  */
3794
3954
  async disconnect(params) {
3795
- const { sessionId } = params;
3796
- const client = this.clients.get(sessionId);
3955
+ const client = this.clients.get(params.sessionId);
3797
3956
  if (client) {
3798
3957
  await client.clearSession();
3799
- this.clients.delete(sessionId);
3958
+ this.clients.delete(params.sessionId);
3800
3959
  } else {
3801
- await sessions.delete(this.userId, sessionId);
3960
+ await sessions.delete(this.userId, params.sessionId);
3802
3961
  }
3803
3962
  return { success: true };
3804
3963
  }
3964
+ // -----------------------------------------------------------------------
3965
+ // OAuth
3966
+ // -----------------------------------------------------------------------
3805
3967
  /**
3806
- * Get an existing client or create and connect a new one for the session.
3968
+ * Completes the OAuth 2.1 authorization code flow for a pending session.
3969
+ *
3970
+ * Loads the stored session (with credentials), creates a fresh `MCPClient`,
3971
+ * and calls `finishAuth` inside a {@link runWithCodeVerifierState} context
3972
+ * so the PKCE code verifier is available without a DB read.
3973
+ *
3974
+ * The session's stored `transportType` is intentionally **omitted** from the
3975
+ * client config — this lets `MCPClient` auto-negotiate (try Streamable HTTP
3976
+ * first, fall back to SSE), which is required for servers like Neon that
3977
+ * only support SSE transport.
3807
3978
  */
3808
- async getOrCreateClient(sessionId) {
3809
- const existing = this.clients.get(sessionId);
3810
- if (existing) {
3811
- return existing;
3812
- }
3813
- const session = await sessions.get(this.userId, sessionId);
3814
- if (!session) {
3815
- throw new Error("Session not found");
3979
+ async finishAuth(params) {
3980
+ const parsed = parseOAuthState(params.state);
3981
+ const sessionId = parsed?.sessionId ?? params.state;
3982
+ const session = await this.requireSession(sessionId);
3983
+ try {
3984
+ const clientId = session.clientId ?? void 0;
3985
+ const clientSecret = session.clientInformation?.client_secret ?? void 0;
3986
+ const client = new MCPClient({
3987
+ userId: this.userId,
3988
+ sessionId,
3989
+ serverId: session.serverId,
3990
+ serverName: session.serverName,
3991
+ serverUrl: session.serverUrl,
3992
+ callbackUrl: session.callbackUrl,
3993
+ headers: session.headers,
3994
+ clientId,
3995
+ clientSecret,
3996
+ hasSession: true,
3997
+ cachedCredentials: { tokens: session.tokens ?? void 0 },
3998
+ sessionStore: this.observedStore
3999
+ });
4000
+ this.attachClientEvents(client);
4001
+ await runWithCodeVerifierState(
4002
+ session.codeVerifier ?? "",
4003
+ "S256",
4004
+ () => client.finishAuth(params.code, params.state)
4005
+ );
4006
+ this.clients.set(sessionId, client);
4007
+ const { result } = await this.listPolicyFilteredTools(sessionId);
4008
+ return { success: true, toolCount: result.tools.length };
4009
+ } catch (error) {
4010
+ this.sendEvent(connectionErrorEvent(sessionId, session.serverId, error, "auth"));
4011
+ throw error;
3816
4012
  }
3817
- const client = new MCPClient({
3818
- userId: this.userId,
3819
- sessionId,
3820
- // These fields are optional in MCPClient, but when rehydrating a known
3821
- // stored session on the server we pass them explicitly to preserve the
3822
- // original transport/connection metadata instead of relying on lazy
3823
- // reloading during initialize().
3824
- serverId: session.serverId,
3825
- serverName: session.serverName,
3826
- serverUrl: session.serverUrl,
3827
- callbackUrl: session.callbackUrl,
3828
- transportType: session.transportType,
3829
- headers: session.headers
3830
- });
3831
- client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3832
- client.onObservabilityEvent((event) => this.sendEvent(event));
3833
- await client.connect();
3834
- this.clients.set(sessionId, client);
3835
- return client;
3836
4013
  }
4014
+ // -----------------------------------------------------------------------
4015
+ // Tool Discovery & Policy
4016
+ // -----------------------------------------------------------------------
3837
4017
  /**
3838
- * Fetches all tools from the remote MCP server and emits a `tools_discovered` event.
4018
+ * Fetches the complete tool list from the remote MCP server and emits a
4019
+ * `tools_discovered` SSE event with two lists:
3839
4020
  *
3840
- * Two lists are always published together:
3841
- * - `tools` policy-filtered list that agents are allowed to call.
3842
- * - `allTools` — the complete, unfiltered list used by the management UI so
3843
- * that blocked tools still appear as checkboxes in the dialog.
4021
+ * - `tools` — policy-filtered (what agents are allowed to call)
4022
+ * - `allTools` complete, unfiltered (used by the management UI)
3844
4023
  *
3845
- * `fetchTools()` is called first (populates the in-memory cache), then
3846
- * `gateway.listTools()` re-uses that cache internally — so only one remote
3847
- * network round-trip is made regardless of how many callers follow.
4024
+ * Only a single network round-trip is made: `fetchTools()` populates the
4025
+ * in-memory cache, then `gateway.listTools()` reuses that cache.
3848
4026
  *
3849
- * @param sessionId - The session whose tools should be discovered.
3850
- * @returns The session record and the policy-filtered tool list.
3851
- * @throws {Error} When the session does not exist in the store.
4027
+ * @param sessionId - The session to discover tools for.
4028
+ * @returns The session record and the filtered tool result.
4029
+ * @throws {Error} If the session does not exist in storage.
3852
4030
  */
3853
4031
  async listPolicyFilteredTools(sessionId) {
3854
- const session = await sessions.get(this.userId, sessionId);
3855
- if (!session) {
3856
- throw new Error("Session not found");
3857
- }
4032
+ const session = await this.requireSession(sessionId);
3858
4033
  const client = await this.getOrCreateClient(sessionId);
3859
4034
  const allTools = await client.fetchTools().catch(() => ({ tools: [] }));
3860
4035
  const gateway = createToolPolicyGateway(this.userId, sessionId, client);
3861
4036
  const result = await gateway.listTools();
3862
- this.emitConnectionEvent({
4037
+ this.sendEvent({
3863
4038
  type: "tools_discovered",
3864
4039
  sessionId,
3865
4040
  serverId: session.serverId ?? "unknown",
@@ -3871,254 +4046,254 @@ var SSEConnectionManager = class {
3871
4046
  return { session, result };
3872
4047
  }
3873
4048
  /**
3874
- * Returns the policy-filtered tool list for a session (agent-facing).
3875
- * Internally re-uses `listPolicyFilteredTools` which also emits a
3876
- * `tools_discovered` SSE event to keep client state up to date.
4049
+ * Returns the policy-filtered tool list for a session.
4050
+ *
4051
+ * Delegates to {@link listPolicyFilteredTools}, which also emits a
4052
+ * `tools_discovered` SSE event to keep the client state current.
3877
4053
  */
3878
4054
  async listTools(params) {
3879
- const { sessionId } = params;
3880
- const { result } = await this.listPolicyFilteredTools(sessionId);
4055
+ const { result } = await this.listPolicyFilteredTools(params.sessionId);
3881
4056
  return { tools: result.tools };
3882
4057
  }
3883
4058
  /**
3884
- * Get all raw tools with effective access state for management UI.
4059
+ * Returns all raw tools annotated with their effective policy state
4060
+ * for display in the management UI.
3885
4061
  */
3886
4062
  async getToolPolicy(params) {
3887
- const { sessionId } = params;
3888
- const session = await sessions.get(this.userId, sessionId);
3889
- if (!session) {
3890
- throw new Error("Session not found");
3891
- }
3892
- const client = await this.getOrCreateClient(sessionId);
4063
+ const session = await this.requireSession(params.sessionId);
4064
+ const client = await this.getOrCreateClient(params.sessionId);
3893
4065
  const allTools = await client.fetchTools();
3894
- const toolPolicy = session.toolPolicy ?? {
4066
+ const policy = session.toolPolicy ?? {
3895
4067
  mode: "all",
3896
4068
  toolIds: [],
3897
4069
  updatedAt: session.updatedAt ?? session.createdAt
3898
4070
  };
3899
4071
  const serverId = session.serverId ?? "unknown";
3900
- const tools = allTools.tools.map((tool) => ({
3901
- ...tool,
3902
- toolId: createToolId(serverId, tool.name),
3903
- allowed: isToolAllowed(toolPolicy, tool.name, session.serverId)
4072
+ const tools = allTools.tools.map((t) => ({
4073
+ ...t,
4074
+ toolId: createToolId(serverId, t.name),
4075
+ allowed: isToolAllowed(policy, t.name, session.serverId)
3904
4076
  }));
3905
4077
  return {
3906
- toolPolicy,
4078
+ toolPolicy: policy,
3907
4079
  tools,
3908
4080
  toolCount: tools.length,
3909
- allowedToolCount: tools.filter((tool) => tool.allowed).length
4081
+ allowedToolCount: tools.filter((t) => t.allowed).length
3910
4082
  };
3911
4083
  }
3912
4084
  /**
3913
- * Persists a new tool access policy for a session and broadcasts the updated
3914
- * filtered tool list to all connected browser clients via a `tools_discovered` event.
4085
+ * Persists a new tool access policy and broadcasts the updated filtered
4086
+ * tool list via a `tools_discovered` SSE event.
3915
4087
  *
3916
- * Both `tools` (policy-filtered) and `allTools` (complete list) are emitted
3917
- * so the management UI can immediately reflect the new checkbox states without
3918
- * an additional round-trip to the server.
3919
- *
3920
- * @param params - Session ID and the new `{ mode, toolIds }` policy to apply.
3921
- * @throws {Error} When the session does not exist or the policy references unknown tool IDs.
4088
+ * @throws {Error} If the session does not exist or the policy references
4089
+ * tool IDs that don't match any known tool.
3922
4090
  */
3923
4091
  async setToolPolicy(params) {
3924
- const { sessionId } = params;
3925
- const session = await sessions.get(this.userId, sessionId);
3926
- if (!session) {
3927
- throw new Error("Session not found");
3928
- }
3929
- const client = await this.getOrCreateClient(sessionId);
4092
+ const session = await this.requireSession(params.sessionId);
4093
+ const client = await this.getOrCreateClient(params.sessionId);
3930
4094
  const allTools = await client.fetchTools();
3931
- const toolPolicy = normalizeToolPolicyForUpdate(params.toolPolicy);
3932
- validateToolPolicyAgainstTools(toolPolicy, allTools.tools, session.serverId);
3933
- await sessions.update(this.userId, sessionId, { toolPolicy });
3934
- const filteredTools = createToolPolicyGateway(this.userId, sessionId, client).filterTools({ ...session, toolPolicy }, allTools.tools);
3935
- this.emitConnectionEvent({
4095
+ const policy = normalizeToolPolicyForUpdate(params.toolPolicy);
4096
+ validateToolPolicyAgainstTools(policy, allTools.tools, session.serverId);
4097
+ await sessions.update(this.userId, params.sessionId, { toolPolicy: policy });
4098
+ const filtered = createToolPolicyGateway(
4099
+ this.userId,
4100
+ params.sessionId,
4101
+ client
4102
+ ).filterTools({ ...session, toolPolicy: policy }, allTools.tools);
4103
+ this.sendEvent({
3936
4104
  type: "tools_discovered",
3937
- sessionId,
4105
+ sessionId: params.sessionId,
3938
4106
  serverId: session.serverId ?? "unknown",
3939
- toolCount: filteredTools.length,
3940
- tools: filteredTools,
4107
+ toolCount: filtered.length,
4108
+ tools: filtered,
3941
4109
  allTools: allTools.tools,
3942
4110
  timestamp: Date.now()
3943
4111
  });
3944
- return {
3945
- success: true,
3946
- toolPolicy,
3947
- tools: filteredTools,
3948
- toolCount: filteredTools.length
3949
- };
3950
- }
3951
- /**
3952
- * Proxies a tool invocation to the remote MCP server.
3953
- * Resolves the client for the given session and delegates to the tool router.
3954
- */
3955
- async callTool(params) {
3956
- const { sessionId, toolName, toolArgs } = params;
3957
- const client = await this.getOrCreateClient(sessionId);
3958
- const result = await createToolPolicyGateway(this.userId, sessionId, client).callTool(toolName, toolArgs);
3959
- const meta = result._meta || {};
3960
- return {
3961
- ...result,
3962
- _meta: {
3963
- ...meta,
3964
- sessionId
3965
- }
3966
- };
4112
+ return { success: true, toolPolicy: policy, tools: filtered, toolCount: filtered.length };
3967
4113
  }
3968
4114
  /**
3969
- * Restore and validate an existing session
4115
+ * Enables or disables a session for agent tool discovery.
4116
+ *
4117
+ * Disabled sessions retain their OAuth tokens and connection metadata
4118
+ * but are hidden from `MultiSessionClient.connect()` and blocked from
4119
+ * RPC tool access. Re-enabling does not require re-authentication.
4120
+ *
4121
+ * @param params - `{ sessionId, enabled: boolean }`
4122
+ * @returns `{ success: true }`
4123
+ * @throws {Error} If the session does not exist.
3970
4124
  */
3971
- async getSession(params) {
3972
- const { sessionId } = params;
3973
- const session = await sessions.get(this.userId, sessionId);
3974
- if (!session) {
3975
- throw new Error("Session not found");
3976
- }
3977
- this.emitConnectionEvent({
3978
- type: "state_changed",
3979
- sessionId,
3980
- serverId: session.serverId ?? "unknown",
3981
- serverName: session.serverName ?? "Unknown",
3982
- serverUrl: session.serverUrl,
3983
- state: "VALIDATING",
3984
- previousState: "DISCONNECTED",
3985
- timestamp: Date.now()
3986
- });
3987
- try {
3988
- const clientMetadata = await this.getResolvedClientMetadata();
3989
- const client = new MCPClient({
3990
- userId: this.userId,
3991
- sessionId,
3992
- // These fields are optional in MCPClient, but when rehydrating a known
3993
- // stored session on the server we pass them explicitly to preserve the
3994
- // original transport/connection metadata instead of relying on lazy
3995
- // reloading during initialize().
3996
- serverId: session.serverId,
3997
- serverName: session.serverName,
3998
- serverUrl: session.serverUrl,
3999
- callbackUrl: session.callbackUrl,
4000
- transportType: session.transportType,
4001
- headers: session.headers,
4002
- ...clientMetadata
4003
- });
4004
- client.onConnectionEvent((event) => this.emitConnectionEvent(event));
4005
- client.onObservabilityEvent((event) => this.sendEvent(event));
4006
- await client.connect();
4007
- this.clients.set(sessionId, client);
4008
- const { result: tools } = await this.listPolicyFilteredTools(sessionId);
4009
- return { success: true, toolCount: tools.tools.length };
4010
- } catch (error) {
4011
- this.emitConnectionEvent({
4012
- type: "error",
4013
- sessionId,
4014
- serverId: session.serverId ?? "unknown",
4015
- error: error instanceof Error ? error.message : "Validation failed",
4016
- errorType: "validation",
4017
- timestamp: Date.now()
4018
- });
4019
- throw error;
4020
- }
4125
+ async updateSession(params) {
4126
+ await this.requireSession(params.sessionId);
4127
+ await sessions.update(this.userId, params.sessionId, { enabled: params.enabled });
4128
+ return { success: true };
4021
4129
  }
4130
+ // -----------------------------------------------------------------------
4131
+ // Tool Execution
4132
+ // -----------------------------------------------------------------------
4022
4133
  /**
4023
- * Complete OAuth authorization flow
4134
+ * Proxies a tool invocation to the remote MCP server.
4135
+ *
4136
+ * Resolves (or creates) the transport for the given session, runs the call
4137
+ * through the tool-policy gateway, and injects `sessionId` into the result
4138
+ * metadata so the client can route the response without scanning all sessions.
4024
4139
  */
4025
- async finishAuth(params) {
4026
- const { code } = params;
4027
- const oauthState = params.state;
4028
- const parsedState = parseOAuthState(oauthState);
4029
- const sessionId = parsedState?.sessionId || oauthState;
4030
- const session = await sessions.get(this.userId, sessionId);
4031
- if (!session) {
4032
- throw new Error("Session not found");
4033
- }
4034
- try {
4035
- const client = new MCPClient({
4036
- userId: this.userId,
4037
- sessionId,
4038
- // These fields are optional in MCPClient, but when rehydrating a known
4039
- // stored session on the server we pass them explicitly to preserve the
4040
- // original connection metadata instead of relying on lazy
4041
- // reloading during initialize().
4042
- serverId: session.serverId,
4043
- serverName: session.serverName,
4044
- serverUrl: session.serverUrl,
4045
- callbackUrl: session.callbackUrl,
4046
- // NOTE: transportType is intentionally omitted here.
4047
- // The session's stored transportType is a placeholder ('streamable-http')
4048
- // set before transport negotiation. Omitting it lets MCPClient auto-negotiate
4049
- // (try streamable_http → SSE fallback), which is critical for servers like
4050
- // Neon that only support SSE transport.
4051
- headers: session.headers
4052
- });
4053
- client.onConnectionEvent((event) => this.emitConnectionEvent(event));
4054
- await client.finishAuth(code, oauthState);
4055
- this.clients.set(sessionId, client);
4056
- const { result: tools } = await this.listPolicyFilteredTools(sessionId);
4057
- return { success: true, toolCount: tools.tools.length };
4058
- } catch (error) {
4059
- this.emitConnectionEvent({
4060
- type: "error",
4061
- sessionId,
4062
- serverId: session.serverId ?? "unknown",
4063
- error: error instanceof Error ? error.message : "OAuth completion failed",
4064
- errorType: "auth",
4065
- timestamp: Date.now()
4066
- });
4067
- throw error;
4068
- }
4140
+ async callTool(params) {
4141
+ const client = await this.getOrCreateClient(params.sessionId);
4142
+ const result = await createToolPolicyGateway(
4143
+ this.userId,
4144
+ params.sessionId,
4145
+ client
4146
+ ).callTool(params.toolName, params.toolArgs);
4147
+ return { ...result, _meta: { ...result._meta ?? {}, sessionId: params.sessionId } };
4069
4148
  }
4070
- /**
4071
- * List prompts from a session
4072
- */
4149
+ // -----------------------------------------------------------------------
4150
+ // Prompts
4151
+ // -----------------------------------------------------------------------
4152
+ /** Lists all prompts exposed by the remote MCP server. */
4073
4153
  async listPrompts(params) {
4074
- const { sessionId } = params;
4075
- const client = await this.getOrCreateClient(sessionId);
4154
+ const client = await this.getOrCreateClient(params.sessionId);
4076
4155
  const result = await client.listPrompts();
4077
4156
  return { prompts: result.prompts };
4078
4157
  }
4079
- /**
4080
- * Get a specific prompt
4081
- */
4158
+ /** Retrieves a specific prompt by name with optional arguments. */
4082
4159
  async getPrompt(params) {
4083
- const { sessionId, name, args } = params;
4084
- const client = await this.getOrCreateClient(sessionId);
4085
- return await client.getPrompt(name, args);
4160
+ const client = await this.getOrCreateClient(params.sessionId);
4161
+ return client.getPrompt(params.name, params.args);
4086
4162
  }
4087
- /**
4088
- * List resources from a session
4089
- */
4163
+ // -----------------------------------------------------------------------
4164
+ // Resources
4165
+ // -----------------------------------------------------------------------
4166
+ /** Lists all resources exposed by the remote MCP server. */
4090
4167
  async listResources(params) {
4091
- const { sessionId } = params;
4092
- const client = await this.getOrCreateClient(sessionId);
4168
+ const client = await this.getOrCreateClient(params.sessionId);
4093
4169
  const result = await client.listResources();
4094
4170
  return { resources: result.resources };
4095
4171
  }
4172
+ /** Reads a specific resource identified by URI. */
4173
+ async readResource(params) {
4174
+ const client = await this.getOrCreateClient(params.sessionId);
4175
+ return client.readResource(params.uri);
4176
+ }
4177
+ // -----------------------------------------------------------------------
4178
+ // Internal Helpers
4179
+ // -----------------------------------------------------------------------
4180
+ /** Resolves client metadata: `getClientMetadata()` → `clientDefaults` → `{}`. */
4181
+ async getResolvedClientMetadata(request) {
4182
+ let metadata = this.options.clientDefaults ? { ...this.options.clientDefaults } : {};
4183
+ if (this.options.getClientMetadata) {
4184
+ metadata = { ...metadata, ...await this.options.getClientMetadata(request) };
4185
+ }
4186
+ return metadata;
4187
+ }
4188
+ /** Ensures the given session exists in storage and throws otherwise. */
4189
+ async requireSession(sessionId) {
4190
+ const session = await sessions.get(this.userId, sessionId, { includeCredentials: true });
4191
+ if (!session) throw new Error(`Session ${sessionId} not found`);
4192
+ return session;
4193
+ }
4194
+ /** Finds an existing session matching the given serverId or serverUrl. */
4195
+ async findExistingSession(serverId, serverUrl) {
4196
+ const all = await sessions.list(this.userId);
4197
+ return all.find((s) => s.serverId === serverId || s.serverUrl === serverUrl);
4198
+ }
4199
+ /** Normalizes a serverId to max 12 chars (DeepSeek/OpenAI 64-char tool-name limit). */
4200
+ normalizeServerId(raw) {
4201
+ return raw && raw.length <= 12 ? raw : generateServerId();
4202
+ }
4096
4203
  /**
4097
- * Read a specific resource
4204
+ * Returns the cached in-memory transport for `sessionId`, or creates one
4205
+ * from the persisted session row (with credentials) and connects it.
4098
4206
  */
4099
- async readResource(params) {
4100
- const { sessionId, uri } = params;
4101
- const client = await this.getOrCreateClient(sessionId);
4102
- return client.readResource(uri);
4207
+ async getOrCreateClient(sessionId) {
4208
+ const existing = this.clients.get(sessionId);
4209
+ if (existing) return existing;
4210
+ const session = await this.requireSession(sessionId);
4211
+ if (session.enabled === false) {
4212
+ throw new Error("Session is disabled \u2014 re-enable it via updateSession to access tools");
4213
+ }
4214
+ const client = this.restoreClient(session);
4215
+ this.attachClientEvents(client);
4216
+ await client.connect();
4217
+ this.clients.set(sessionId, client);
4218
+ return client;
4219
+ }
4220
+ /**
4221
+ * Builds an `MCPClient` from a stored session row.
4222
+ *
4223
+ * Extracts `clientId` and `clientSecret` from the session's credential
4224
+ * fields and passes `hasSession: true` + `cachedCredentials` so the client
4225
+ * can skip redundant existence checks and credential reads.
4226
+ */
4227
+ restoreClient(session) {
4228
+ const clientId = session.clientId ?? void 0;
4229
+ const clientSecret = session.clientInformation?.client_secret ?? void 0;
4230
+ return new MCPClient({
4231
+ userId: this.userId,
4232
+ sessionId: session.sessionId,
4233
+ serverId: session.serverId,
4234
+ serverName: session.serverName,
4235
+ serverUrl: session.serverUrl,
4236
+ callbackUrl: session.callbackUrl,
4237
+ transportType: session.transportType,
4238
+ headers: session.headers,
4239
+ clientId,
4240
+ clientSecret,
4241
+ hasSession: true,
4242
+ cachedCredentials: { tokens: session.tokens ?? void 0 },
4243
+ sessionStore: this.observedStore
4244
+ });
4103
4245
  }
4104
4246
  /**
4105
- * Emit connection event
4247
+ * Wires connection and observability events from the client into the
4248
+ * unified observability channel. Connection events go to `sendEvent`
4249
+ * (SSE stream); observability events go to `emitObs` (user callback
4250
+ * + SSE stream).
4106
4251
  */
4107
- emitConnectionEvent(event) {
4108
- this.sendEvent(event);
4252
+ attachClientEvents(client) {
4253
+ client.onConnectionEvent((e) => this.sendEvent(e));
4254
+ client.onObservabilityEvent((e) => this.sendEvent(e));
4109
4255
  }
4110
4256
  /**
4111
- * Cleanup and close all connections
4257
+ * Registers the client in the in-memory cache and attaches its event
4258
+ * listeners to the unified observability channel.
4112
4259
  */
4113
- async dispose() {
4114
- this.isActive = false;
4115
- if (this.heartbeatTimer) {
4116
- clearInterval(this.heartbeatTimer);
4260
+ cacheClient(sessionId, client) {
4261
+ this.attachClientEvents(client);
4262
+ this.clients.set(sessionId, client);
4263
+ }
4264
+ /**
4265
+ * Attempts a `client.connect()` and, on success, discovers tools.
4266
+ *
4267
+ * If the server responds with `UnauthorizedError`, the session is
4268
+ * treated as pending OAuth — a successful result is returned with
4269
+ * the sessionId so the browser client can redirect to the auth URL.
4270
+ *
4271
+ * For any other error an `error` connection event is emitted and the
4272
+ * client is removed from the in-memory cache before re-throwing.
4273
+ */
4274
+ async connectAndDiscover(client, sessionId, serverId) {
4275
+ try {
4276
+ await client.connect();
4277
+ await this.listPolicyFilteredTools(sessionId);
4278
+ return { sessionId, success: true };
4279
+ } catch (error) {
4280
+ if (error instanceof UnauthorizedError) {
4281
+ this.clients.delete(sessionId);
4282
+ return { sessionId, success: true };
4283
+ }
4284
+ this.sendEvent(connectionErrorEvent(sessionId, serverId, error, "connection"));
4285
+ this.clients.delete(sessionId);
4286
+ throw error;
4117
4287
  }
4118
- await Promise.all(
4119
- Array.from(this.clients.values()).map((client) => client.disconnect())
4120
- );
4121
- this.clients.clear();
4288
+ }
4289
+ /** Starts the periodic heartbeat timer. */
4290
+ startHeartbeat() {
4291
+ const ms = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS;
4292
+ this.heartbeatTimer = setInterval(() => {
4293
+ if (this.active) {
4294
+ this.sendEvent({ level: "debug", message: "heartbeat", timestamp: Date.now() });
4295
+ }
4296
+ }, ms);
4122
4297
  }
4123
4298
  };
4124
4299
  function createSSEHandler(options) {
@@ -4147,8 +4322,7 @@ function createSSEHandler(options) {
4147
4322
  });
4148
4323
  req.on("end", async () => {
4149
4324
  try {
4150
- const request = JSON.parse(body);
4151
- await manager.handleRequest(request);
4325
+ await manager.handleRequest(JSON.parse(body));
4152
4326
  } catch {
4153
4327
  }
4154
4328
  });
@@ -4323,8 +4497,11 @@ exports.StorageOAuthClientProvider = StorageOAuthClientProvider;
4323
4497
  exports.UnauthorizedError = UnauthorizedError;
4324
4498
  exports.createNextMcpHandler = createNextMcpHandler;
4325
4499
  exports.createSSEHandler = createSSEHandler;
4500
+ exports.decryptObject = decryptObject;
4501
+ exports.encryptObject = encryptObject;
4326
4502
  exports.onSessionMutation = onSessionMutation;
4327
4503
  exports.sanitizeServerLabel = sanitizeServerLabel;
4328
4504
  exports.sessions = sessions;
4505
+ exports.withDbObservability = withDbObservability;
4329
4506
  //# sourceMappingURL=index.js.map
4330
4507
  //# sourceMappingURL=index.js.map