@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
package/dist/index.js CHANGED
@@ -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
  var appBridge = require('@modelcontextprotocol/ext-apps/app-bridge');
13
14
 
14
15
  function _interopNamespace(e) {
@@ -44,9 +45,9 @@ var __export = (target, all) => {
44
45
  };
45
46
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
46
47
 
47
- // node_modules/tsup/assets/cjs_shims.js
48
+ // ../../node_modules/tsup/assets/cjs_shims.js
48
49
  var init_cjs_shims = __esm({
49
- "node_modules/tsup/assets/cjs_shims.js"() {
50
+ "../../node_modules/tsup/assets/cjs_shims.js"() {
50
51
  }
51
52
  });
52
53
 
@@ -332,7 +333,6 @@ var RedisStorageBackend = class {
332
333
  constructor(redis2) {
333
334
  this.redis = redis2;
334
335
  __publicField(this, "KEY_PREFIX", "mcp:session:");
335
- __publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
336
336
  __publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
337
337
  __publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
338
338
  }
@@ -351,9 +351,6 @@ var RedisStorageBackend = class {
351
351
  getSessionKey(userId, sessionId) {
352
352
  return `${this.KEY_PREFIX}${userId}:${sessionId}`;
353
353
  }
354
- getCredentialsKey(userId, sessionId) {
355
- return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
356
- }
357
354
  /**
358
355
  * Generates Redis key for tracking all sessions for a user
359
356
  * @private
@@ -443,48 +440,54 @@ var RedisStorageBackend = class {
443
440
  }
444
441
  async patchCredentials(userId, sessionId, data) {
445
442
  const sessionKey = this.getSessionKey(userId, sessionId);
446
- const credentialsKey = this.getCredentialsKey(userId, sessionId);
447
- const sessionExists = await this.redis.exists(sessionKey);
448
- if (!sessionExists) {
443
+ const currentStr = await this.redis.get(sessionKey);
444
+ if (!currentStr) {
449
445
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
450
446
  }
451
- const session = await this.get(userId, sessionId);
452
- const currentTtl = await this.redis.ttl(sessionKey);
453
- const effectiveTtl = currentTtl > 0 && session ? currentTtl : resolveSessionRedisTtlSeconds(session ?? { status: "pending" });
454
- const currentStr = await this.redis.get(credentialsKey);
455
- const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
456
- const credentials = { ...current, ...data, sessionId, userId };
457
- if (effectiveTtl > 0) {
458
- await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
459
- } else {
460
- await this.redis.set(credentialsKey, JSON.stringify(credentials));
461
- }
447
+ const current = JSON.parse(currentStr);
448
+ const updated = { ...current, ...data, sessionId, userId };
449
+ const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
450
+ await this.redis.set(sessionKey, JSON.stringify(updated), "EX", effectiveTtl);
462
451
  }
463
- async get(userId, sessionId) {
452
+ async get(userId, sessionId, options) {
464
453
  try {
465
454
  const sessionKey = this.getSessionKey(userId, sessionId);
466
455
  const sessionDataStr = await this.redis.get(sessionKey);
467
- if (!sessionDataStr) {
468
- return null;
456
+ if (!sessionDataStr) return null;
457
+ const session = normalizeStoredSession(JSON.parse(sessionDataStr));
458
+ if (!options?.includeCredentials) {
459
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
460
+ return sessionOnly;
469
461
  }
470
- const session = JSON.parse(sessionDataStr);
471
- return normalizeStoredSession(session);
462
+ return session;
472
463
  } catch (error) {
473
464
  console.error("[RedisStorageBackend] Failed to get session:", error);
474
465
  return null;
475
466
  }
476
467
  }
477
468
  async getCredentials(userId, sessionId) {
478
- const session = await this.get(userId, sessionId);
469
+ const session = await this.get(userId, sessionId, { includeCredentials: true });
479
470
  if (!session) return null;
480
- const credentialsStr = await this.redis.get(this.getCredentialsKey(userId, sessionId));
481
- return credentialsStr ? JSON.parse(credentialsStr) : { sessionId, userId };
471
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
472
+ return {
473
+ sessionId,
474
+ userId,
475
+ clientInformation,
476
+ tokens,
477
+ codeVerifier,
478
+ codeVerifierChallenge,
479
+ codeVerifierNonce,
480
+ clientId,
481
+ oauthState
482
+ };
482
483
  }
483
484
  async clearCredentials(userId, sessionId) {
484
485
  await this.patchCredentials(userId, sessionId, {
485
486
  clientInformation: null,
486
487
  tokens: null,
487
488
  codeVerifier: null,
489
+ codeVerifierChallenge: null,
490
+ codeVerifierNonce: null,
488
491
  clientId: null,
489
492
  oauthState: null
490
493
  });
@@ -517,10 +520,9 @@ var RedisStorageBackend = class {
517
520
  async delete(userId, sessionId) {
518
521
  try {
519
522
  const sessionKey = this.getSessionKey(userId, sessionId);
520
- const credentialsKey = this.getCredentialsKey(userId, sessionId);
521
523
  const userIdKey = this.getUserIdKey(userId);
522
524
  await this.redis.srem(userIdKey, sessionId);
523
- await this.redis.del(sessionKey, credentialsKey);
525
+ await this.redis.del(sessionKey);
524
526
  } catch (error) {
525
527
  console.error("[RedisStorageBackend] Failed to remove session:", error);
526
528
  }
@@ -551,9 +553,8 @@ var RedisStorageBackend = class {
551
553
  async clearAll() {
552
554
  try {
553
555
  const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
554
- const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
555
556
  const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
556
- const allKeys = [...keys, ...credentialKeys, ...userIdKeys];
557
+ const allKeys = [...keys, ...userIdKeys];
557
558
  if (allKeys.length > 0) {
558
559
  await this.redis.del(...allKeys);
559
560
  }
@@ -577,7 +578,6 @@ var RedisStorageBackend = class {
577
578
  const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
578
579
  if (staleSessionIds.length > 0) {
579
580
  await this.redis.srem(userIdKey, ...staleSessionIds);
580
- await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
581
581
  }
582
582
  const remainingCount = await this.redis.scard(userIdKey);
583
583
  if (remainingCount === 0) {
@@ -603,7 +603,6 @@ var MemoryStorageBackend = class {
603
603
  constructor() {
604
604
  // Map<userId:sessionId, Session>
605
605
  __publicField(this, "sessions", /* @__PURE__ */ new Map());
606
- __publicField(this, "credentials", /* @__PURE__ */ new Map());
607
606
  // Map<userId, Set<sessionId>>
608
607
  __publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
609
608
  }
@@ -641,26 +640,46 @@ var MemoryStorageBackend = class {
641
640
  }
642
641
  async patchCredentials(userId, sessionId, data) {
643
642
  const sessionKey = this.getSessionKey(userId, sessionId);
644
- if (!this.sessions.has(sessionKey)) {
643
+ const current = this.sessions.get(sessionKey);
644
+ if (!current) {
645
645
  throw new Error(`Session ${sessionId} not found`);
646
646
  }
647
- const current = this.credentials.get(sessionKey) ?? { sessionId, userId };
648
- this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
647
+ this.sessions.set(sessionKey, { ...current, ...data, sessionId, userId });
649
648
  }
650
- async get(userId, sessionId) {
649
+ async get(userId, sessionId, options) {
651
650
  const sessionKey = this.getSessionKey(userId, sessionId);
652
- return this.sessions.get(sessionKey) || null;
651
+ const session = this.sessions.get(sessionKey) || null;
652
+ if (!session) return session;
653
+ if (!options?.includeCredentials) {
654
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
655
+ return sessionOnly;
656
+ }
657
+ return session;
653
658
  }
654
659
  async getCredentials(userId, sessionId) {
655
660
  const sessionKey = this.getSessionKey(userId, sessionId);
656
- if (!this.sessions.has(sessionKey)) return null;
657
- return this.credentials.get(sessionKey) ?? { sessionId, userId };
661
+ const session = this.sessions.get(sessionKey);
662
+ if (!session) return null;
663
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
664
+ return {
665
+ sessionId,
666
+ userId,
667
+ clientInformation,
668
+ tokens,
669
+ codeVerifier,
670
+ codeVerifierChallenge,
671
+ codeVerifierNonce,
672
+ clientId,
673
+ oauthState
674
+ };
658
675
  }
659
676
  async clearCredentials(userId, sessionId) {
660
677
  await this.patchCredentials(userId, sessionId, {
661
678
  clientInformation: null,
662
679
  tokens: null,
663
680
  codeVerifier: null,
681
+ codeVerifierChallenge: null,
682
+ codeVerifierNonce: null,
664
683
  clientId: null,
665
684
  oauthState: null
666
685
  });
@@ -684,7 +703,6 @@ var MemoryStorageBackend = class {
684
703
  async delete(userId, sessionId) {
685
704
  const sessionKey = this.getSessionKey(userId, sessionId);
686
705
  this.sessions.delete(sessionKey);
687
- this.credentials.delete(sessionKey);
688
706
  const set = this.userIdSessions.get(userId);
689
707
  if (set) {
690
708
  set.delete(sessionId);
@@ -698,14 +716,12 @@ var MemoryStorageBackend = class {
698
716
  }
699
717
  async clearAll() {
700
718
  this.sessions.clear();
701
- this.credentials.clear();
702
719
  this.userIdSessions.clear();
703
720
  }
704
721
  async cleanupExpired() {
705
722
  for (const [key, session] of this.sessions.entries()) {
706
723
  if (!isSessionExpired(session)) continue;
707
724
  this.sessions.delete(key);
708
- this.credentials.delete(key);
709
725
  const set = this.userIdSessions.get(session.userId);
710
726
  if (set) {
711
727
  set.delete(session.sessionId);
@@ -728,7 +744,6 @@ var FileStorageBackend = class {
728
744
  constructor(options = {}) {
729
745
  __publicField(this, "filePath");
730
746
  __publicField(this, "memoryCache", null);
731
- __publicField(this, "credentialsCache", null);
732
747
  __publicField(this, "initialized", false);
733
748
  this.filePath = options.path || "./sessions.json";
734
749
  }
@@ -743,22 +758,15 @@ var FileStorageBackend = class {
743
758
  const data = await fs2.promises.readFile(this.filePath, "utf-8");
744
759
  const json = JSON.parse(data);
745
760
  this.memoryCache = /* @__PURE__ */ new Map();
746
- this.credentialsCache = /* @__PURE__ */ new Map();
747
761
  if (Array.isArray(json.sessions)) {
748
762
  json.sessions.forEach((s) => {
749
763
  const session = normalizeStoredSession(s);
750
764
  this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
751
765
  });
752
766
  }
753
- if (Array.isArray(json.credentials)) {
754
- json.credentials.forEach((c) => {
755
- this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
756
- });
757
- }
758
767
  } catch (error) {
759
768
  if (error.code === "ENOENT") {
760
769
  this.memoryCache = /* @__PURE__ */ new Map();
761
- this.credentialsCache = /* @__PURE__ */ new Map();
762
770
  await this.flush();
763
771
  } else {
764
772
  console.error("[FileStorage] Failed to load sessions:", error);
@@ -772,10 +780,9 @@ var FileStorageBackend = class {
772
780
  if (!this.initialized) await this.init();
773
781
  }
774
782
  async flush() {
775
- if (!this.memoryCache || !this.credentialsCache) return;
783
+ if (!this.memoryCache) return;
776
784
  await fs2.promises.writeFile(this.filePath, JSON.stringify({
777
- sessions: Array.from(this.memoryCache.values()),
778
- credentials: Array.from(this.credentialsCache.values())
785
+ sessions: Array.from(this.memoryCache.values())
779
786
  }, null, 2), "utf-8");
780
787
  }
781
788
  getSessionKey(userId, sessionId) {
@@ -810,29 +817,49 @@ var FileStorageBackend = class {
810
817
  async patchCredentials(userId, sessionId, data) {
811
818
  await this.ensureInitialized();
812
819
  const sessionKey = this.getSessionKey(userId, sessionId);
813
- if (!this.memoryCache.has(sessionKey)) {
820
+ const current = this.memoryCache.get(sessionKey);
821
+ if (!current) {
814
822
  throw new Error(`Session ${sessionId} not found`);
815
823
  }
816
- const current = this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
817
- this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
824
+ this.memoryCache.set(sessionKey, { ...current, ...data, sessionId, userId });
818
825
  await this.flush();
819
826
  }
820
- async get(userId, sessionId) {
827
+ async get(userId, sessionId, options) {
821
828
  await this.ensureInitialized();
822
829
  const sessionKey = this.getSessionKey(userId, sessionId);
823
- return this.memoryCache.get(sessionKey) || null;
830
+ const session = this.memoryCache.get(sessionKey) || null;
831
+ if (!session) return null;
832
+ if (!options?.includeCredentials) {
833
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
834
+ return sessionOnly;
835
+ }
836
+ return session;
824
837
  }
825
838
  async getCredentials(userId, sessionId) {
826
839
  await this.ensureInitialized();
827
840
  const sessionKey = this.getSessionKey(userId, sessionId);
828
- if (!this.memoryCache.has(sessionKey)) return null;
829
- return this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
841
+ const session = this.memoryCache.get(sessionKey);
842
+ if (!session) return null;
843
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
844
+ return {
845
+ sessionId,
846
+ userId,
847
+ clientInformation,
848
+ tokens,
849
+ codeVerifier,
850
+ codeVerifierChallenge,
851
+ codeVerifierNonce,
852
+ clientId,
853
+ oauthState
854
+ };
830
855
  }
831
856
  async clearCredentials(userId, sessionId) {
832
857
  await this.patchCredentials(userId, sessionId, {
833
858
  clientInformation: null,
834
859
  tokens: null,
835
860
  codeVerifier: null,
861
+ codeVerifierChallenge: null,
862
+ codeVerifierNonce: null,
836
863
  clientId: null,
837
864
  oauthState: null
838
865
  });
@@ -849,7 +876,6 @@ var FileStorageBackend = class {
849
876
  await this.ensureInitialized();
850
877
  const sessionKey = this.getSessionKey(userId, sessionId);
851
878
  const deleted = this.memoryCache.delete(sessionKey);
852
- this.credentialsCache.delete(sessionKey);
853
879
  if (deleted) {
854
880
  await this.flush();
855
881
  }
@@ -861,7 +887,6 @@ var FileStorageBackend = class {
861
887
  async clearAll() {
862
888
  await this.ensureInitialized();
863
889
  this.memoryCache.clear();
864
- this.credentialsCache.clear();
865
890
  await this.flush();
866
891
  }
867
892
  async cleanupExpired() {
@@ -870,7 +895,6 @@ var FileStorageBackend = class {
870
895
  for (const [key, session] of this.memoryCache.entries()) {
871
896
  if (!isSessionExpired(session)) continue;
872
897
  this.memoryCache.delete(key);
873
- this.credentialsCache.delete(key);
874
898
  changed = true;
875
899
  }
876
900
  if (changed) {
@@ -887,12 +911,10 @@ var SqliteStorage = class {
887
911
  constructor(options = {}) {
888
912
  __publicField(this, "db", null);
889
913
  __publicField(this, "table");
890
- __publicField(this, "credentialsTable");
891
914
  __publicField(this, "initialized", false);
892
915
  __publicField(this, "dbPath");
893
916
  this.dbPath = options.path || "./sessions.db";
894
917
  this.table = options.table || "mcp_sessions";
895
- this.credentialsTable = `${this.table}_credentials`;
896
918
  }
897
919
  async init() {
898
920
  if (this.initialized) return;
@@ -912,13 +934,6 @@ var SqliteStorage = class {
912
934
  expiresAt INTEGER
913
935
  );
914
936
  CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
915
- CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
916
- sessionId TEXT NOT NULL,
917
- userId TEXT NOT NULL,
918
- data TEXT NOT NULL,
919
- PRIMARY KEY (userId, sessionId),
920
- FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
921
- );
922
937
  `);
923
938
  this.initialized = true;
924
939
  console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
@@ -975,41 +990,54 @@ var SqliteStorage = class {
975
990
  }
976
991
  async patchCredentials(userId, sessionId, data) {
977
992
  this.ensureInitialized();
978
- if (!await this.get(userId, sessionId)) {
993
+ const currentSession = await this.get(userId, sessionId, { includeCredentials: true });
994
+ if (!currentSession) {
979
995
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
980
996
  }
981
- const current = await this.getCredentials(userId, sessionId) ?? { sessionId, userId };
982
- const credentials = { ...current, ...data, sessionId, userId };
997
+ const updated = { ...currentSession, ...data, sessionId, userId };
983
998
  const stmt = this.db.prepare(
984
- `INSERT INTO ${this.credentialsTable} (sessionId, userId, data)
985
- VALUES (?, ?, ?)
986
- ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
999
+ `UPDATE ${this.table} SET data = ? WHERE sessionId = ? AND userId = ?`
987
1000
  );
988
- stmt.run(sessionId, userId, JSON.stringify(credentials));
1001
+ stmt.run(JSON.stringify(updated), sessionId, userId);
989
1002
  }
990
- async get(userId, sessionId) {
1003
+ async get(userId, sessionId, options) {
991
1004
  this.ensureInitialized();
992
1005
  const stmt = this.db.prepare(
993
1006
  `SELECT data FROM ${this.table} WHERE sessionId = ? AND userId = ?`
994
1007
  );
995
1008
  const row = stmt.get(sessionId, userId);
996
1009
  if (!row) return null;
997
- return normalizeStoredSession(JSON.parse(row.data));
1010
+ const session = normalizeStoredSession(JSON.parse(row.data));
1011
+ if (!options?.includeCredentials) {
1012
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
1013
+ return sessionOnly;
1014
+ }
1015
+ return session;
998
1016
  }
999
1017
  async getCredentials(userId, sessionId) {
1000
1018
  this.ensureInitialized();
1001
- if (!await this.get(userId, sessionId)) return null;
1002
- const stmt = this.db.prepare(
1003
- `SELECT data FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
1004
- );
1005
- const row = stmt.get(sessionId, userId);
1006
- return row ? JSON.parse(row.data) : { sessionId, userId };
1019
+ const session = await this.get(userId, sessionId, { includeCredentials: true });
1020
+ if (!session) return null;
1021
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
1022
+ return {
1023
+ sessionId,
1024
+ userId,
1025
+ clientInformation,
1026
+ tokens,
1027
+ codeVerifier,
1028
+ codeVerifierChallenge,
1029
+ codeVerifierNonce,
1030
+ clientId,
1031
+ oauthState
1032
+ };
1007
1033
  }
1008
1034
  async clearCredentials(userId, sessionId) {
1009
1035
  await this.patchCredentials(userId, sessionId, {
1010
1036
  clientInformation: null,
1011
1037
  tokens: null,
1012
1038
  codeVerifier: null,
1039
+ codeVerifierChallenge: null,
1040
+ codeVerifierNonce: null,
1013
1041
  clientId: null,
1014
1042
  oauthState: null
1015
1043
  });
@@ -1035,9 +1063,6 @@ var SqliteStorage = class {
1035
1063
  const stmt = this.db.prepare(
1036
1064
  `DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
1037
1065
  );
1038
- this.db.prepare(
1039
- `DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
1040
- ).run(sessionId, userId);
1041
1066
  stmt.run(sessionId, userId);
1042
1067
  }
1043
1068
  async listAllIds() {
@@ -1048,7 +1073,6 @@ var SqliteStorage = class {
1048
1073
  }
1049
1074
  async clearAll() {
1050
1075
  this.ensureInitialized();
1051
- this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
1052
1076
  const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
1053
1077
  stmt.run();
1054
1078
  }
@@ -1062,14 +1086,6 @@ var SqliteStorage = class {
1062
1086
  deleteStmt.run(row.sessionId, row.userId);
1063
1087
  }
1064
1088
  }
1065
- this.db.prepare(
1066
- `DELETE FROM ${this.credentialsTable}
1067
- WHERE NOT EXISTS (
1068
- SELECT 1 FROM ${this.table}
1069
- WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
1070
- AND ${this.table}.userId = ${this.credentialsTable}.userId
1071
- )`
1072
- ).run();
1073
1089
  }
1074
1090
  async disconnect() {
1075
1091
  if (this.db) {
@@ -1110,8 +1126,8 @@ function encryptObject(data) {
1110
1126
  }
1111
1127
  try {
1112
1128
  const text = JSON.stringify(data);
1113
- const iv = crypto.randomBytes(IV_LENGTH);
1114
- const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
1129
+ const iv = crypto$1.randomBytes(IV_LENGTH);
1130
+ const cipher = crypto$1.createCipheriv(ALGORITHM, key, iv);
1115
1131
  let encrypted = cipher.update(text, "utf-8", "hex");
1116
1132
  encrypted += cipher.final("hex");
1117
1133
  const authTag = cipher.getAuthTag().toString("hex");
@@ -1139,7 +1155,7 @@ function decryptObject(data) {
1139
1155
  const iv = Buffer.from(parts[2], "hex");
1140
1156
  const authTag = Buffer.from(parts[3], "hex");
1141
1157
  const encryptedText = parts[4];
1142
- const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
1158
+ const decipher = crypto$1.createDecipheriv(ALGORITHM, key, iv);
1143
1159
  decipher.setAuthTag(authTag);
1144
1160
  let decrypted = decipher.update(encryptedText, "hex", "utf-8");
1145
1161
  decrypted += decipher.final("utf-8");
@@ -1157,7 +1173,6 @@ var SupabaseStorageBackend = class {
1157
1173
  }
1158
1174
  async init() {
1159
1175
  await this.assertTable("mcp_sessions", "session_id");
1160
- await this.assertTable("mcp_credentials", "session_id");
1161
1176
  console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
1162
1177
  }
1163
1178
  async assertTable(table, column) {
@@ -1188,18 +1203,13 @@ var SupabaseStorageBackend = class {
1188
1203
  headers: decryptObject(row.headers),
1189
1204
  authUrl: row.auth_url,
1190
1205
  status: row.status ?? "pending",
1191
- toolPolicy: normalizeToolPolicy(row.tool_policy)
1192
- };
1193
- }
1194
- mapRowToCredentials(row, userId, sessionId) {
1195
- return {
1196
- sessionId,
1197
- userId,
1198
- clientInformation: decryptObject(row?.client_information),
1199
- tokens: decryptObject(row?.tokens),
1200
- codeVerifier: decryptObject(row?.code_verifier),
1201
- clientId: row?.client_id,
1202
- oauthState: row?.oauth_state
1206
+ toolPolicy: normalizeToolPolicy(row.tool_policy),
1207
+ clientInformation: decryptObject(row.client_information),
1208
+ tokens: decryptObject(row.tokens),
1209
+ codeVerifier: decryptObject(row.code_verifier),
1210
+ clientId: row.client_id,
1211
+ oauthState: row.oauth_state,
1212
+ enabled: row.enabled ?? true
1203
1213
  };
1204
1214
  }
1205
1215
  hasCredentialData(data) {
@@ -1257,6 +1267,7 @@ var SupabaseStorageBackend = class {
1257
1267
  if ("headers" in data) updateData.headers = encryptObject(data.headers);
1258
1268
  if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
1259
1269
  if ("toolPolicy" in data) updateData.tool_policy = normalizeToolPolicy(data.toolPolicy);
1270
+ if ("enabled" in data) updateData.enabled = data.enabled;
1260
1271
  const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
1261
1272
  let updatedRows = null;
1262
1273
  if (shouldUpdateSession) {
@@ -1278,23 +1289,22 @@ var SupabaseStorageBackend = class {
1278
1289
  }
1279
1290
  async patchCredentials(userId, sessionId, data) {
1280
1291
  if (!this.hasCredentialData(data)) return;
1281
- const row = {
1282
- user_id: userId,
1283
- session_id: sessionId,
1292
+ const updateData = {
1284
1293
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
1285
1294
  };
1286
- if ("clientInformation" in data) row.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
1287
- if ("tokens" in data) row.tokens = data.tokens == null ? null : encryptObject(data.tokens);
1288
- if ("codeVerifier" in data) row.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
1289
- if ("clientId" in data) row.client_id = data.clientId ?? null;
1290
- if ("oauthState" in data) row.oauth_state = data.oauthState ?? null;
1291
- const { error } = await this.supabase.from("mcp_credentials").upsert(row, { onConflict: "user_id,session_id" });
1295
+ if ("clientInformation" in data) updateData.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
1296
+ if ("tokens" in data) updateData.tokens = data.tokens == null ? null : encryptObject(data.tokens);
1297
+ if ("codeVerifier" in data) updateData.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
1298
+ if ("clientId" in data) updateData.client_id = data.clientId ?? null;
1299
+ if ("oauthState" in data) updateData.oauth_state = data.oauthState ?? null;
1300
+ const { error } = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId);
1292
1301
  if (error) {
1293
1302
  throw new Error(`Failed to update credentials: ${error.message}`);
1294
1303
  }
1295
1304
  }
1296
- async get(userId, sessionId) {
1297
- const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
1305
+ async get(userId, sessionId, options) {
1306
+ 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";
1307
+ const { data, error } = await this.supabase.from("mcp_sessions").select(selection).eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
1298
1308
  if (error) {
1299
1309
  console.error("[SupabaseStorage] Failed to get session:", error);
1300
1310
  return null;
@@ -1303,19 +1313,21 @@ var SupabaseStorageBackend = class {
1303
1313
  return this.mapRowToSessionData(data);
1304
1314
  }
1305
1315
  async getCredentials(userId, sessionId) {
1306
- const { data, error } = await this.supabase.from("mcp_credentials").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
1316
+ 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();
1307
1317
  if (error) {
1308
1318
  console.error("[SupabaseStorage] Failed to get credentials:", error);
1309
1319
  return null;
1310
1320
  }
1311
- if (data) {
1312
- return this.mapRowToCredentials(data, userId, sessionId);
1313
- }
1314
- const { data: sessionRows, error: sessionError } = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
1315
- if (sessionError || !sessionRows || sessionRows.length === 0) {
1316
- return null;
1317
- }
1318
- return { sessionId, userId };
1321
+ if (!data) return null;
1322
+ return {
1323
+ sessionId,
1324
+ userId,
1325
+ clientInformation: decryptObject(data.client_information),
1326
+ tokens: decryptObject(data.tokens),
1327
+ codeVerifier: decryptObject(data.code_verifier),
1328
+ clientId: data.client_id,
1329
+ oauthState: data.oauth_state
1330
+ };
1319
1331
  }
1320
1332
  async list(userId) {
1321
1333
  const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
@@ -1326,7 +1338,14 @@ var SupabaseStorageBackend = class {
1326
1338
  return data.map((row) => this.mapRowToSessionData(row));
1327
1339
  }
1328
1340
  async clearCredentials(userId, sessionId) {
1329
- const { error } = await this.supabase.from("mcp_credentials").delete().eq("user_id", userId).eq("session_id", sessionId);
1341
+ const { error } = await this.supabase.from("mcp_sessions").update({
1342
+ client_information: null,
1343
+ tokens: null,
1344
+ code_verifier: null,
1345
+ client_id: null,
1346
+ oauth_state: null,
1347
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1348
+ }).eq("user_id", userId).eq("session_id", sessionId);
1330
1349
  if (error) {
1331
1350
  throw new Error(`Failed to clear credentials: ${error.message}`);
1332
1351
  }
@@ -1354,10 +1373,6 @@ var SupabaseStorageBackend = class {
1354
1373
  return data.map((row) => row.session_id);
1355
1374
  }
1356
1375
  async clearAll() {
1357
- const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
1358
- if (credentialsError) {
1359
- console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
1360
- }
1361
1376
  const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
1362
1377
  if (error) {
1363
1378
  console.error("[SupabaseStorage] Failed to clear sessions:", error);
@@ -1384,16 +1399,12 @@ var NeonStorageBackend = class {
1384
1399
  constructor(sql, options = {}) {
1385
1400
  this.sql = sql;
1386
1401
  __publicField(this, "tableName");
1387
- __publicField(this, "credentialsTableName");
1388
1402
  const schema = options.schema || "public";
1389
1403
  const table = options.table || "mcp_sessions";
1390
- const credentialsTable = options.credentialsTable || "mcp_credentials";
1391
1404
  this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
1392
- this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
1393
1405
  }
1394
1406
  async init() {
1395
1407
  await this.assertTable(this.tableName, "mcp_sessions");
1396
- await this.assertTable(this.credentialsTableName, "mcp_credentials");
1397
1408
  console.log("[mcp-ts][Storage] Neon: storage tables verified.");
1398
1409
  }
1399
1410
  async assertTable(qualifiedName, displayName) {
@@ -1431,18 +1442,13 @@ var NeonStorageBackend = class {
1431
1442
  headers: decryptObject(row.headers),
1432
1443
  authUrl: row.auth_url ?? void 0,
1433
1444
  status: row.status ?? "pending",
1434
- toolPolicy: normalizeToolPolicy(row.tool_policy)
1435
- };
1436
- }
1437
- mapRowToCredentials(row, userId, sessionId) {
1438
- return {
1439
- sessionId,
1440
- userId,
1445
+ toolPolicy: normalizeToolPolicy(row.tool_policy),
1441
1446
  clientInformation: decryptObject(row.client_information),
1442
1447
  tokens: decryptObject(row.tokens),
1443
1448
  codeVerifier: decryptObject(row.code_verifier),
1444
1449
  clientId: row.client_id ?? void 0,
1445
- oauthState: row.oauth_state
1450
+ oauthState: row.oauth_state,
1451
+ enabled: row.enabled ?? true
1446
1452
  };
1447
1453
  }
1448
1454
  hasCredentialData(data) {
@@ -1512,7 +1518,7 @@ var NeonStorageBackend = class {
1512
1518
  const updatedSession = { ...currentSession, ...data };
1513
1519
  const status = updatedSession.status ?? "pending";
1514
1520
  const expiresAt = resolveSessionExpiresAt(status);
1515
- 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;
1521
+ 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;
1516
1522
  if (shouldUpdateSession) {
1517
1523
  const setClauses = [];
1518
1524
  const values = [];
@@ -1535,6 +1541,9 @@ var NeonStorageBackend = class {
1535
1541
  const toolPolicy = normalizeToolPolicy(updatedSession.toolPolicy, policyUpdatedAt) ?? { mode: "all", toolIds: [], updatedAt: policyUpdatedAt };
1536
1542
  addSet("tool_policy", toolPolicy);
1537
1543
  }
1544
+ if ("enabled" in data) {
1545
+ addSet("enabled", updatedSession.enabled);
1546
+ }
1538
1547
  setClauses.push("updated_at = now()");
1539
1548
  const updatedRows = await this.sql.query(
1540
1549
  `UPDATE ${this.tableName}
@@ -1550,45 +1559,45 @@ var NeonStorageBackend = class {
1550
1559
  }
1551
1560
  async patchCredentials(userId, sessionId, data) {
1552
1561
  if (!this.hasCredentialData(data)) return;
1553
- await this.sql.query(
1554
- `INSERT INTO ${this.credentialsTableName} (
1555
- user_id,
1556
- session_id,
1557
- client_information,
1558
- tokens,
1559
- code_verifier,
1560
- client_id,
1561
- oauth_state,
1562
- updated_at
1563
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, now())
1564
- ON CONFLICT (user_id, session_id)
1565
- DO UPDATE SET
1566
- client_information = CASE WHEN $8 THEN EXCLUDED.client_information ELSE ${this.credentialsTableName}.client_information END,
1567
- tokens = CASE WHEN $9 THEN EXCLUDED.tokens ELSE ${this.credentialsTableName}.tokens END,
1568
- code_verifier = CASE WHEN $10 THEN EXCLUDED.code_verifier ELSE ${this.credentialsTableName}.code_verifier END,
1569
- client_id = CASE WHEN $11 THEN EXCLUDED.client_id ELSE ${this.credentialsTableName}.client_id END,
1570
- oauth_state = CASE WHEN $12 THEN EXCLUDED.oauth_state ELSE ${this.credentialsTableName}.oauth_state END,
1571
- updated_at = now()`,
1572
- [
1573
- userId,
1574
- sessionId,
1575
- "clientInformation" in data ? data.clientInformation == null ? null : encryptObject(data.clientInformation) : null,
1576
- "tokens" in data ? data.tokens == null ? null : encryptObject(data.tokens) : null,
1577
- "codeVerifier" in data ? data.codeVerifier == null ? null : encryptObject(data.codeVerifier) : null,
1578
- "clientId" in data ? data.clientId ?? null : null,
1579
- "oauthState" in data ? data.oauthState ?? null : null,
1580
- "clientInformation" in data,
1581
- "tokens" in data,
1582
- "codeVerifier" in data,
1583
- "clientId" in data,
1584
- "oauthState" in data
1585
- ]
1562
+ const setClauses = [];
1563
+ const values = [];
1564
+ let paramIndex = 1;
1565
+ const addSet = (column, value) => {
1566
+ setClauses.push(`${column} = $${paramIndex++}`);
1567
+ values.push(value);
1568
+ };
1569
+ if ("clientInformation" in data) {
1570
+ addSet("client_information", data.clientInformation == null ? null : encryptObject(data.clientInformation));
1571
+ }
1572
+ if ("tokens" in data) {
1573
+ addSet("tokens", data.tokens == null ? null : encryptObject(data.tokens));
1574
+ }
1575
+ if ("codeVerifier" in data) {
1576
+ addSet("code_verifier", data.codeVerifier == null ? null : encryptObject(data.codeVerifier));
1577
+ }
1578
+ if ("clientId" in data) {
1579
+ addSet("client_id", data.clientId ?? null);
1580
+ }
1581
+ if ("oauthState" in data) {
1582
+ addSet("oauth_state", data.oauthState ?? null);
1583
+ }
1584
+ setClauses.push("updated_at = now()");
1585
+ const updatedRows = await this.sql.query(
1586
+ `UPDATE ${this.tableName}
1587
+ SET ${setClauses.join(", ")}
1588
+ WHERE user_id = $${paramIndex++} AND session_id = $${paramIndex++}
1589
+ RETURNING id`,
1590
+ [...values, userId, sessionId]
1586
1591
  );
1592
+ if (updatedRows.length === 0) {
1593
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
1594
+ }
1587
1595
  }
1588
- async get(userId, sessionId) {
1596
+ async get(userId, sessionId, options) {
1589
1597
  try {
1598
+ 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";
1590
1599
  const rows = await this.sql.query(
1591
- `SELECT * FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1600
+ `SELECT ${selection} FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1592
1601
  [userId, sessionId]
1593
1602
  );
1594
1603
  if (!rows[0]) return null;
@@ -1600,18 +1609,22 @@ var NeonStorageBackend = class {
1600
1609
  }
1601
1610
  async getCredentials(userId, sessionId) {
1602
1611
  try {
1603
- const credentialRows = await this.sql.query(
1604
- `SELECT * FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
1605
- [userId, sessionId]
1606
- );
1607
- if (credentialRows[0]) {
1608
- return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
1609
- }
1610
- const sessionRows = await this.sql.query(
1611
- `SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1612
+ const rows = await this.sql.query(
1613
+ `SELECT client_information, tokens, code_verifier, client_id, oauth_state
1614
+ FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1612
1615
  [userId, sessionId]
1613
1616
  );
1614
- return sessionRows[0] ? { sessionId, userId } : null;
1617
+ if (!rows[0]) return null;
1618
+ const row = rows[0];
1619
+ return {
1620
+ sessionId,
1621
+ userId,
1622
+ clientInformation: decryptObject(row.client_information),
1623
+ tokens: decryptObject(row.tokens),
1624
+ codeVerifier: decryptObject(row.code_verifier),
1625
+ clientId: row.client_id ?? void 0,
1626
+ oauthState: row.oauth_state
1627
+ };
1615
1628
  } catch (error) {
1616
1629
  console.error("[NeonStorage] Failed to get credentials:", error);
1617
1630
  return null;
@@ -1632,7 +1645,9 @@ var NeonStorageBackend = class {
1632
1645
  async clearCredentials(userId, sessionId) {
1633
1646
  try {
1634
1647
  await this.sql.query(
1635
- `DELETE FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
1648
+ `UPDATE ${this.tableName}
1649
+ SET client_information = null, tokens = null, code_verifier = null, client_id = null, oauth_state = null, updated_at = now()
1650
+ WHERE user_id = $1 AND session_id = $2`,
1636
1651
  [userId, sessionId]
1637
1652
  );
1638
1653
  } catch (error) {
@@ -1674,7 +1689,6 @@ var NeonStorageBackend = class {
1674
1689
  }
1675
1690
  async clearAll() {
1676
1691
  try {
1677
- await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
1678
1692
  await this.sql.query(`DELETE FROM ${this.tableName}`);
1679
1693
  } catch (error) {
1680
1694
  console.error("[NeonStorage] Failed to clear sessions:", error);
@@ -1919,6 +1933,60 @@ function onSessionMutation(listener) {
1919
1933
  sessionMutationListeners.delete(listener);
1920
1934
  };
1921
1935
  }
1936
+ function withDbObservability(store, emit) {
1937
+ const readMethods = /* @__PURE__ */ new Set([
1938
+ "get",
1939
+ "getCredentials",
1940
+ "list",
1941
+ "listIds",
1942
+ "listAllIds"
1943
+ ]);
1944
+ return new Proxy(store, {
1945
+ get(target, prop, receiver) {
1946
+ const original = Reflect.get(target, prop, receiver);
1947
+ if (typeof original !== "function") return original;
1948
+ return (...args) => {
1949
+ const method = prop;
1950
+ const isRead = readMethods.has(prop);
1951
+ const start = performance.now();
1952
+ const emitEvent = (error) => {
1953
+ emit({
1954
+ type: isRead ? "db:read" : "db:write",
1955
+ level: error ? "error" : "debug",
1956
+ message: `${method}(${args.map(
1957
+ (a) => typeof a === "string" ? a.slice(0, 24) : typeof a
1958
+ ).join(", ")})`,
1959
+ sessionId: typeof args[1] === "string" ? args[1] : void 0,
1960
+ payload: {
1961
+ method,
1962
+ argTypes: args.map((a) => typeof a),
1963
+ durationMs: performance.now() - start,
1964
+ ...error ? { error } : {}
1965
+ },
1966
+ timestamp: Date.now()
1967
+ });
1968
+ };
1969
+ try {
1970
+ const result = original.apply(target, args);
1971
+ if (result instanceof Promise) {
1972
+ return result.then((r) => {
1973
+ emitEvent();
1974
+ return r;
1975
+ }).catch((e) => {
1976
+ emitEvent(String(e));
1977
+ throw e;
1978
+ });
1979
+ }
1980
+ emitEvent();
1981
+ return result;
1982
+ } catch (e) {
1983
+ emitEvent(String(e));
1984
+ throw e;
1985
+ }
1986
+ };
1987
+ }
1988
+ });
1989
+ }
1922
1990
  var sessions = new Proxy({}, {
1923
1991
  get(_target, prop) {
1924
1992
  return async (...args) => {
@@ -1938,6 +2006,24 @@ var sessions = new Proxy({}, {
1938
2006
  });
1939
2007
 
1940
2008
  // src/server/mcp/storage-oauth-provider.ts
2009
+ var codeVerifierContext = new async_hooks.AsyncLocalStorage();
2010
+ function runWithCodeVerifierState(verifier, method, fn) {
2011
+ return codeVerifierContext.run({ verifier, method }, fn);
2012
+ }
2013
+ function base64UrlEncode(bytes) {
2014
+ let binary = "";
2015
+ for (const byte of bytes) {
2016
+ binary += String.fromCharCode(byte);
2017
+ }
2018
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2019
+ }
2020
+ async function createCodeChallenge(verifier) {
2021
+ const digest = await crypto.subtle.digest(
2022
+ "SHA-256",
2023
+ new TextEncoder().encode(verifier)
2024
+ );
2025
+ return base64UrlEncode(new Uint8Array(digest));
2026
+ }
1941
2027
  var StorageOAuthClientProvider = class {
1942
2028
  /**
1943
2029
  * Creates a new session-backed OAuth provider
@@ -1953,8 +2039,12 @@ var StorageOAuthClientProvider = class {
1953
2039
  __publicField(this, "logoUri");
1954
2040
  __publicField(this, "policyUri");
1955
2041
  __publicField(this, "clientSecret");
2042
+ __publicField(this, "_store");
1956
2043
  __publicField(this, "_authUrl");
1957
2044
  __publicField(this, "_clientId");
2045
+ __publicField(this, "_cachedCodeVerifier");
2046
+ __publicField(this, "_hasCodeVerifier", false);
2047
+ __publicField(this, "_cachedTokens");
1958
2048
  __publicField(this, "onRedirectCallback");
1959
2049
  this.userId = options.userId;
1960
2050
  this.serverId = options.serverId;
@@ -1966,6 +2056,8 @@ var StorageOAuthClientProvider = class {
1966
2056
  this.policyUri = options.policyUri;
1967
2057
  this._clientId = options.clientId;
1968
2058
  this.clientSecret = options.clientSecret;
2059
+ this._cachedTokens = options.cachedTokens;
2060
+ this._store = options.sessionStore ?? sessions;
1969
2061
  this.onRedirectCallback = options.onRedirect;
1970
2062
  }
1971
2063
  get clientMetadata() {
@@ -1993,7 +2085,7 @@ var StorageOAuthClientProvider = class {
1993
2085
  * @private
1994
2086
  */
1995
2087
  async getCredentials() {
1996
- const data = await sessions.getCredentials(this.userId, this.sessionId);
2088
+ const data = await this._store.getCredentials(this.userId, this.sessionId);
1997
2089
  if (!data) {
1998
2090
  return { userId: this.userId, sessionId: this.sessionId };
1999
2091
  }
@@ -2006,35 +2098,33 @@ var StorageOAuthClientProvider = class {
2006
2098
  * @throws Error if session doesn't exist (session must be created by controller layer)
2007
2099
  */
2008
2100
  async patchCredentials(data) {
2009
- await sessions.patchCredentials(this.userId, this.sessionId, data);
2101
+ await this._store.patchCredentials(this.userId, this.sessionId, data);
2010
2102
  }
2011
2103
  /**
2012
- * Retrieves stored OAuth client information
2104
+ * Retrieves stored OAuth client information.
2013
2105
  */
2014
2106
  async clientInformation() {
2015
- const data = await this.getCredentials();
2016
- if (data.clientId && !this._clientId) {
2017
- this._clientId = data.clientId;
2018
- }
2019
- if (this._clientId && !data.clientId) {
2020
- await this.patchCredentials({
2021
- clientId: this._clientId,
2022
- clientInformation: {
2023
- client_id: this._clientId,
2024
- ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2025
- }
2026
- });
2107
+ if (this._clientId) {
2108
+ return {
2109
+ client_id: this._clientId,
2110
+ ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2111
+ };
2027
2112
  }
2028
- if (data.clientInformation) {
2029
- return data.clientInformation;
2113
+ const data = await this.getCredentials();
2114
+ if (this._cachedTokens === void 0) {
2115
+ this._cachedTokens = data.tokens ?? null;
2030
2116
  }
2031
- if (!this._clientId) {
2032
- return void 0;
2117
+ if (data.clientId) {
2118
+ this._clientId = data.clientId;
2119
+ if (data.clientInformation) {
2120
+ return data.clientInformation;
2121
+ }
2122
+ return {
2123
+ client_id: data.clientId,
2124
+ ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2125
+ };
2033
2126
  }
2034
- return {
2035
- client_id: this._clientId,
2036
- ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2037
- };
2127
+ return void 0;
2038
2128
  }
2039
2129
  /**
2040
2130
  * Stores OAuth client information
@@ -2051,11 +2141,30 @@ var StorageOAuthClientProvider = class {
2051
2141
  */
2052
2142
  async saveTokens(tokens) {
2053
2143
  await this.patchCredentials({ tokens });
2144
+ this._cachedTokens = tokens;
2145
+ }
2146
+ /**
2147
+ * Persists static client credentials to DB immediately on creation.
2148
+ * Ensures getOrCreateClient() finds them on rehydration, even when the
2149
+ * server allows anonymous connect and never triggers DCR.
2150
+ */
2151
+ async initializeCredentials() {
2152
+ if (this._clientId) {
2153
+ await this.patchCredentials({
2154
+ clientId: this._clientId,
2155
+ clientInformation: {
2156
+ client_id: this._clientId,
2157
+ ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2158
+ }
2159
+ });
2160
+ }
2054
2161
  }
2055
2162
  get authUrl() {
2056
2163
  return this._authUrl;
2057
2164
  }
2058
2165
  async state() {
2166
+ this._cachedCodeVerifier = void 0;
2167
+ this._hasCodeVerifier = false;
2059
2168
  const nonce = nanoid.nanoid(32);
2060
2169
  await this.patchCredentials({
2061
2170
  oauthState: {
@@ -2076,7 +2185,7 @@ var StorageOAuthClientProvider = class {
2076
2185
  if (parsed.sessionId !== this.sessionId) {
2077
2186
  return { valid: false, error: "OAuth state mismatch" };
2078
2187
  }
2079
- const data = await sessions.getCredentials(this.userId, parsed.sessionId);
2188
+ const data = await this._store.getCredentials(this.userId, parsed.sessionId);
2080
2189
  if (!data) {
2081
2190
  return { valid: false, error: "Session not found" };
2082
2191
  }
@@ -2101,49 +2210,77 @@ var StorageOAuthClientProvider = class {
2101
2210
  }
2102
2211
  async redirectToAuthorization(authUrl) {
2103
2212
  this._authUrl = authUrl.toString();
2104
- await sessions.update(this.userId, this.sessionId, { authUrl: this._authUrl });
2213
+ const codeChallenge = authUrl.searchParams.get("code_challenge");
2214
+ const state = authUrl.searchParams.get("state");
2215
+ if (this._cachedCodeVerifier && codeChallenge && state) {
2216
+ const expectedChallenge = await createCodeChallenge(this._cachedCodeVerifier);
2217
+ if (expectedChallenge === codeChallenge) {
2218
+ const parsed = parseOAuthState(state);
2219
+ if (parsed) {
2220
+ await this.patchCredentials({
2221
+ codeVerifier: this._cachedCodeVerifier
2222
+ });
2223
+ }
2224
+ }
2225
+ }
2105
2226
  if (this.onRedirectCallback) {
2106
2227
  this.onRedirectCallback(authUrl.toString());
2107
2228
  }
2108
2229
  }
2109
2230
  async invalidateCredentials(scope) {
2110
2231
  if (scope === "all") {
2111
- await sessions.delete(this.userId, this.sessionId);
2232
+ this._cachedTokens = void 0;
2233
+ await this._store.delete(this.userId, this.sessionId);
2112
2234
  } else {
2113
2235
  const updates = {};
2114
2236
  if (scope === "client") {
2115
2237
  updates.clientInformation = null;
2116
2238
  updates.clientId = null;
2117
2239
  } else if (scope === "tokens") {
2240
+ this._cachedTokens = void 0;
2118
2241
  updates.tokens = null;
2119
2242
  } else if (scope === "verifier") {
2243
+ this._cachedCodeVerifier = void 0;
2244
+ this._hasCodeVerifier = false;
2120
2245
  updates.codeVerifier = null;
2121
2246
  }
2122
2247
  await this.patchCredentials(updates);
2123
2248
  }
2124
2249
  }
2125
2250
  async saveCodeVerifier(verifier) {
2126
- const data = await this.getCredentials();
2127
- if (data.codeVerifier) {
2251
+ if (this._hasCodeVerifier) {
2128
2252
  return;
2129
2253
  }
2130
- await this.patchCredentials({ codeVerifier: verifier });
2254
+ this._cachedCodeVerifier = verifier;
2255
+ this._hasCodeVerifier = true;
2131
2256
  }
2132
2257
  async codeVerifier() {
2133
- const data = await this.getCredentials();
2134
- if (data.clientId && !this._clientId) {
2135
- this._clientId = data.clientId;
2258
+ if (this._cachedCodeVerifier) {
2259
+ return this._cachedCodeVerifier;
2136
2260
  }
2137
- if (!data.codeVerifier) {
2138
- throw new Error("No code verifier found");
2261
+ const ctx = codeVerifierContext.getStore();
2262
+ if (ctx?.verifier) {
2263
+ return ctx.verifier;
2264
+ }
2265
+ const data = await this.getCredentials();
2266
+ if (data.codeVerifier) {
2267
+ return data.codeVerifier;
2139
2268
  }
2140
- return data.codeVerifier;
2269
+ throw new Error("No code verifier found");
2141
2270
  }
2142
2271
  async deleteCodeVerifier() {
2143
- await this.patchCredentials({ codeVerifier: null });
2272
+ this._cachedCodeVerifier = void 0;
2273
+ this._hasCodeVerifier = false;
2274
+ await this.patchCredentials({
2275
+ codeVerifier: null
2276
+ });
2144
2277
  }
2145
2278
  async tokens() {
2279
+ if (this._cachedTokens !== void 0) {
2280
+ return this._cachedTokens ?? void 0;
2281
+ }
2146
2282
  const data = await this.getCredentials();
2283
+ this._cachedTokens = data.tokens ?? null;
2147
2284
  if (data.clientId && !this._clientId) {
2148
2285
  this._clientId = data.clientId;
2149
2286
  }
@@ -2306,23 +2443,10 @@ var MCPClient = class {
2306
2443
  __publicField(this, "client");
2307
2444
  __publicField(this, "oauthProvider", null);
2308
2445
  __publicField(this, "transport", null);
2309
- __publicField(this, "userId");
2310
- __publicField(this, "serverId");
2311
- __publicField(this, "sessionId");
2312
- __publicField(this, "serverName");
2313
- __publicField(this, "transportType");
2314
- __publicField(this, "serverUrl");
2315
- __publicField(this, "callbackUrl");
2316
- __publicField(this, "onRedirect");
2317
- __publicField(this, "clientId");
2318
- __publicField(this, "clientSecret");
2319
- __publicField(this, "headers");
2320
- /** OAuth Client Metadata */
2321
- __publicField(this, "clientName");
2322
- __publicField(this, "clientUri");
2323
- __publicField(this, "logoUri");
2324
- __publicField(this, "policyUri");
2446
+ __publicField(this, "config");
2325
2447
  __publicField(this, "createdAt");
2448
+ __publicField(this, "_serverInfo");
2449
+ __publicField(this, "_store");
2326
2450
  /** Event emitters for connection lifecycle */
2327
2451
  __publicField(this, "_onConnectionEvent", new Emitter());
2328
2452
  __publicField(this, "onConnectionEvent", this._onConnectionEvent.event);
@@ -2338,21 +2462,8 @@ var MCPClient = class {
2338
2462
  * the memory when the client is no longer needed.
2339
2463
  */
2340
2464
  __publicField(this, "cachedTools", null);
2341
- this.serverUrl = options.serverUrl;
2342
- this.serverName = options.serverName;
2343
- this.callbackUrl = options.callbackUrl;
2344
- this.onRedirect = options.onRedirect;
2345
- this.userId = options.userId;
2346
- this.serverId = options.serverId;
2347
- this.sessionId = options.sessionId;
2348
- this.transportType = options.transportType;
2349
- this.clientId = options.clientId;
2350
- this.clientSecret = options.clientSecret;
2351
- this.headers = options.headers;
2352
- this.clientName = options.clientName;
2353
- this.clientUri = options.clientUri;
2354
- this.logoUri = options.logoUri;
2355
- this.policyUri = options.policyUri;
2465
+ this.config = { ...options };
2466
+ this._store = options.sessionStore ?? sessions;
2356
2467
  this.client = new index_js.Client(
2357
2468
  {
2358
2469
  name: MCP_CLIENT_NAME,
@@ -2369,6 +2480,21 @@ var MCPClient = class {
2369
2480
  }
2370
2481
  );
2371
2482
  }
2483
+ /** Shared session-shaped data for ensureSession and saveSession */
2484
+ get session() {
2485
+ return {
2486
+ sessionId: this.config.sessionId,
2487
+ userId: this.config.userId,
2488
+ serverId: this.config.serverId,
2489
+ serverName: this.config.serverName,
2490
+ serverUrl: this.config.serverUrl,
2491
+ callbackUrl: this.config.callbackUrl,
2492
+ transportType: this.config.transportType || "streamable-http",
2493
+ headers: this.config.headers,
2494
+ createdAt: this.createdAt ?? Date.now(),
2495
+ updatedAt: Date.now()
2496
+ };
2497
+ }
2372
2498
  /**
2373
2499
  * Emit a connection state change event
2374
2500
  * @private
@@ -2376,13 +2502,13 @@ var MCPClient = class {
2376
2502
  emitStateChange(newState) {
2377
2503
  const previousState = this.currentState;
2378
2504
  this.currentState = newState;
2379
- if (!this.serverId) return;
2505
+ if (!this.config.serverId) return;
2380
2506
  this._onConnectionEvent.fire({
2381
2507
  type: "state_changed",
2382
- sessionId: this.sessionId,
2383
- serverId: this.serverId,
2384
- serverName: this.serverName || this.serverId,
2385
- serverUrl: this.serverUrl || "",
2508
+ sessionId: this.config.sessionId,
2509
+ serverId: this.config.serverId,
2510
+ serverName: this.config.serverName || this.config.serverId,
2511
+ serverUrl: this.config.serverUrl || "",
2386
2512
  createdAt: this.createdAt,
2387
2513
  state: newState,
2388
2514
  previousState,
@@ -2393,8 +2519,8 @@ var MCPClient = class {
2393
2519
  level: "info",
2394
2520
  message: `Connection state: ${previousState} \u2192 ${newState}`,
2395
2521
  displayMessage: `State changed to ${newState}`,
2396
- sessionId: this.sessionId,
2397
- serverId: this.serverId,
2522
+ sessionId: this.config.sessionId,
2523
+ serverId: this.config.serverId,
2398
2524
  payload: { previousState, newState },
2399
2525
  timestamp: Date.now(),
2400
2526
  id: nanoid.nanoid()
@@ -2405,11 +2531,11 @@ var MCPClient = class {
2405
2531
  * @private
2406
2532
  */
2407
2533
  emitError(error, errorType = "unknown") {
2408
- if (!this.serverId) return;
2534
+ if (!this.config.serverId) return;
2409
2535
  this._onConnectionEvent.fire({
2410
2536
  type: "error",
2411
- sessionId: this.sessionId,
2412
- serverId: this.serverId,
2537
+ sessionId: this.config.sessionId,
2538
+ serverId: this.config.serverId,
2413
2539
  error,
2414
2540
  errorType,
2415
2541
  timestamp: Date.now()
@@ -2419,8 +2545,8 @@ var MCPClient = class {
2419
2545
  level: "error",
2420
2546
  message: error,
2421
2547
  displayMessage: error,
2422
- sessionId: this.sessionId,
2423
- serverId: this.serverId,
2548
+ sessionId: this.config.sessionId,
2549
+ serverId: this.config.serverId,
2424
2550
  payload: { errorType, error },
2425
2551
  timestamp: Date.now(),
2426
2552
  id: nanoid.nanoid()
@@ -2431,11 +2557,11 @@ var MCPClient = class {
2431
2557
  * @private
2432
2558
  */
2433
2559
  emitProgress(message) {
2434
- if (!this.serverId) return;
2560
+ if (!this.config.serverId) return;
2435
2561
  this._onConnectionEvent.fire({
2436
2562
  type: "progress",
2437
- sessionId: this.sessionId,
2438
- serverId: this.serverId,
2563
+ sessionId: this.config.sessionId,
2564
+ serverId: this.config.serverId,
2439
2565
  message,
2440
2566
  timestamp: Date.now()
2441
2567
  });
@@ -2453,14 +2579,14 @@ var MCPClient = class {
2453
2579
  * @private
2454
2580
  */
2455
2581
  getTransport(type) {
2456
- if (!this.serverUrl) {
2582
+ if (!this.config.serverUrl) {
2457
2583
  throw new Error("Server URL is required to create transport");
2458
2584
  }
2459
- const baseUrl = new URL(this.serverUrl);
2460
- const hasAuthorizationHeader = Object.keys(this.headers || {}).some((key) => key.toLowerCase() === "authorization");
2585
+ const baseUrl = new URL(this.config.serverUrl);
2586
+ const hasAuthorizationHeader = Object.keys(this.config.headers || {}).some((key) => key.toLowerCase() === "authorization");
2461
2587
  const transportOptions = {
2462
2588
  ...!hasAuthorizationHeader && { authProvider: this.oauthProvider },
2463
- ...this.headers && { requestInit: { headers: this.headers } },
2589
+ ...this.config.headers && { requestInit: { headers: this.config.headers } },
2464
2590
  /**
2465
2591
  * Custom fetch implementation to handle connection timeouts.
2466
2592
  * Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
@@ -2498,78 +2624,74 @@ var MCPClient = class {
2498
2624
  * @private
2499
2625
  */
2500
2626
  async ensureSession() {
2501
- if (this.oauthProvider) {
2502
- return;
2503
- }
2627
+ if (this.oauthProvider) return;
2504
2628
  this.emitStateChange("INITIALIZING");
2505
2629
  this.emitProgress("Loading session configuration...");
2506
- let existingSession = null;
2507
- if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
2508
- existingSession = await sessions.get(this.userId, this.sessionId);
2509
- if (!existingSession) {
2510
- throw new Error(`Session not found: ${this.sessionId}`);
2630
+ if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
2631
+ const existingSession2 = await this._store.get(this.config.userId, this.config.sessionId);
2632
+ if (!existingSession2) {
2633
+ throw new Error(`Session not found: ${this.config.sessionId}`);
2511
2634
  }
2512
- this.serverUrl = this.serverUrl || existingSession.serverUrl;
2513
- this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
2514
- this.serverName = this.serverName || existingSession.serverName;
2515
- this.serverId = this.serverId || existingSession.serverId || "unknown";
2516
- this.headers = this.headers || existingSession.headers;
2517
- this.createdAt = existingSession.createdAt;
2518
- }
2519
- if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
2635
+ this.config.serverUrl = this.config.serverUrl || existingSession2.serverUrl;
2636
+ this.config.callbackUrl = this.config.callbackUrl || existingSession2.callbackUrl;
2637
+ this.config.serverName = this.config.serverName || existingSession2.serverName;
2638
+ this.config.serverId = this.config.serverId || existingSession2.serverId || "unknown";
2639
+ this.config.headers = this.config.headers || existingSession2.headers;
2640
+ this.createdAt = existingSession2.createdAt;
2641
+ }
2642
+ if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
2520
2643
  throw new Error("Missing required connection metadata");
2521
2644
  }
2522
- if (!this.oauthProvider) {
2523
- if (!this.serverId) {
2524
- throw new Error("serverId required for OAuth provider initialization");
2525
- }
2526
- this.oauthProvider = new StorageOAuthClientProvider({
2527
- userId: this.userId,
2528
- serverId: this.serverId,
2529
- sessionId: this.sessionId,
2530
- redirectUrl: this.callbackUrl,
2531
- clientName: this.clientName,
2532
- clientUri: this.clientUri,
2533
- logoUri: this.logoUri,
2534
- policyUri: this.policyUri,
2535
- clientId: this.clientId,
2536
- clientSecret: this.clientSecret,
2537
- onRedirect: (redirectUrl) => {
2538
- if (this.onRedirect) {
2539
- this.onRedirect(redirectUrl);
2540
- }
2645
+ this.oauthProvider = new StorageOAuthClientProvider({
2646
+ userId: this.config.userId,
2647
+ serverId: this.config.serverId,
2648
+ sessionId: this.config.sessionId,
2649
+ redirectUrl: this.config.callbackUrl,
2650
+ clientName: this.config.clientName,
2651
+ clientUri: this.config.clientUri,
2652
+ logoUri: this.config.logoUri,
2653
+ policyUri: this.config.policyUri,
2654
+ clientId: this.config.clientId,
2655
+ clientSecret: this.config.clientSecret,
2656
+ cachedTokens: this.config.cachedCredentials?.tokens,
2657
+ sessionStore: this._store,
2658
+ onRedirect: (redirectUrl) => {
2659
+ if (this.config.serverId) {
2660
+ this._onConnectionEvent.fire({
2661
+ type: "auth_required",
2662
+ sessionId: this.config.sessionId,
2663
+ serverId: this.config.serverId,
2664
+ authUrl: redirectUrl,
2665
+ timestamp: Date.now()
2666
+ });
2541
2667
  }
2542
- });
2543
- }
2544
- if (existingSession === null) {
2545
- existingSession = await sessions.get(this.userId, this.sessionId);
2546
- }
2547
- if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
2668
+ if (this.config.onRedirect) {
2669
+ this.config.onRedirect(redirectUrl);
2670
+ }
2671
+ }
2672
+ });
2673
+ const existingSession = this.config.hasSession ? {} : await this._store.get(this.config.userId, this.config.sessionId);
2674
+ if (!existingSession) {
2548
2675
  this.createdAt = Date.now();
2549
2676
  const updatedAt = this.createdAt;
2550
2677
  this._onObservabilityEvent.fire({
2551
2678
  type: "mcp:client:session_created",
2552
2679
  level: "info",
2553
- message: `Creating pending session ${this.sessionId} for connection setup`,
2554
- sessionId: this.sessionId,
2555
- serverId: this.serverId,
2680
+ message: `Creating pending session ${this.config.sessionId} for connection setup`,
2681
+ sessionId: this.config.sessionId,
2682
+ serverId: this.config.serverId,
2556
2683
  timestamp: Date.now(),
2557
2684
  id: nanoid.nanoid()
2558
2685
  });
2559
- await sessions.create({
2560
- sessionId: this.sessionId,
2561
- userId: this.userId,
2562
- serverId: this.serverId,
2563
- serverName: this.serverName,
2564
- serverUrl: this.serverUrl,
2565
- callbackUrl: this.callbackUrl,
2566
- transportType: this.transportType || "streamable-http",
2567
- headers: this.headers,
2568
- createdAt: this.createdAt,
2686
+ await this._store.create({
2687
+ ...this.session,
2569
2688
  updatedAt,
2570
2689
  status: "pending"
2571
2690
  });
2572
2691
  }
2692
+ if (!existingSession && this.oauthProvider instanceof StorageOAuthClientProvider) {
2693
+ await this.oauthProvider.initializeCredentials();
2694
+ }
2573
2695
  }
2574
2696
  /**
2575
2697
  * Saves current session state to the session store
@@ -2577,34 +2699,14 @@ var MCPClient = class {
2577
2699
  * @param status - Session lifecycle status used by storage cleanup
2578
2700
  * @private
2579
2701
  */
2580
- async saveSession(status = "active", existingSession) {
2581
- if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
2702
+ async saveSession(status = "active") {
2703
+ if (!this.config.sessionId || !this.config.serverId || !this.config.serverUrl || !this.config.callbackUrl) {
2582
2704
  return;
2583
2705
  }
2584
- const sessionData = {
2585
- sessionId: this.sessionId,
2586
- userId: this.userId,
2587
- serverId: this.serverId,
2588
- serverName: this.serverName,
2589
- serverUrl: this.serverUrl,
2590
- callbackUrl: this.callbackUrl,
2591
- transportType: this.transportType || "streamable-http",
2592
- headers: this.headers,
2593
- createdAt: this.createdAt || Date.now(),
2594
- updatedAt: Date.now(),
2706
+ await this._store.update(this.config.userId, this.config.sessionId, {
2707
+ ...this.session,
2595
2708
  status
2596
- };
2597
- if (status === "active") {
2598
- sessionData.authUrl = null;
2599
- }
2600
- if (existingSession === void 0) {
2601
- existingSession = await sessions.get(this.userId, this.sessionId);
2602
- }
2603
- if (existingSession) {
2604
- await sessions.update(this.userId, this.sessionId, sessionData);
2605
- } else {
2606
- await sessions.create(sessionData);
2607
- }
2709
+ });
2608
2710
  }
2609
2711
  /**
2610
2712
  * Removes transient setup/auth sessions without masking the original error.
@@ -2612,7 +2714,7 @@ var MCPClient = class {
2612
2714
  */
2613
2715
  async deleteTransientSession() {
2614
2716
  try {
2615
- await sessions.delete(this.userId, this.sessionId);
2717
+ await this._store.delete(this.config.userId, this.config.sessionId);
2616
2718
  } catch {
2617
2719
  }
2618
2720
  }
@@ -2622,7 +2724,7 @@ var MCPClient = class {
2622
2724
  * @private
2623
2725
  */
2624
2726
  async tryConnect() {
2625
- const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
2727
+ const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
2626
2728
  let lastError;
2627
2729
  for (const currentType of transportsToTry) {
2628
2730
  const isLastAttempt = currentType === transportsToTry[transportsToTry.length - 1];
@@ -2630,6 +2732,7 @@ var MCPClient = class {
2630
2732
  const transport = this.getTransport(currentType);
2631
2733
  this.transport = transport;
2632
2734
  await this.client.connect(transport);
2735
+ this._serverInfo = this.client.getServerVersion();
2633
2736
  return { transportType: currentType };
2634
2737
  } catch (error) {
2635
2738
  lastError = error;
@@ -2645,8 +2748,8 @@ var MCPClient = class {
2645
2748
  this._onObservabilityEvent.fire({
2646
2749
  level: "warn",
2647
2750
  message: `Transport ${currentType} failed, falling back`,
2648
- sessionId: this.sessionId,
2649
- serverId: this.serverId,
2751
+ sessionId: this.config.sessionId,
2752
+ serverId: this.config.serverId,
2650
2753
  metadata: {
2651
2754
  failedTransport: currentType,
2652
2755
  error: errorMessage
@@ -2689,15 +2792,15 @@ var MCPClient = class {
2689
2792
  try {
2690
2793
  this.emitStateChange("CONNECTING");
2691
2794
  const { transportType } = await this.tryConnect();
2692
- this.transportType = transportType;
2795
+ this.config.transportType = transportType;
2693
2796
  this.emitStateChange("CONNECTED");
2694
2797
  this.emitProgress("Connected successfully");
2695
2798
  this._onObservabilityEvent.fire({
2696
2799
  type: "mcp:client:session_saved",
2697
2800
  level: "info",
2698
- message: `Saving active session ${this.sessionId} (connect success)`,
2699
- sessionId: this.sessionId,
2700
- serverId: this.serverId,
2801
+ message: `Saving active session ${this.config.sessionId} (connect success)`,
2802
+ sessionId: this.config.sessionId,
2803
+ serverId: this.config.serverId,
2701
2804
  timestamp: Date.now(),
2702
2805
  id: nanoid.nanoid()
2703
2806
  });
@@ -2720,23 +2823,23 @@ var MCPClient = class {
2720
2823
  this._onObservabilityEvent.fire({
2721
2824
  type: "mcp:client:session_saved",
2722
2825
  level: "info",
2723
- message: `Saving pending OAuth session ${this.sessionId}`,
2724
- sessionId: this.sessionId,
2725
- serverId: this.serverId,
2826
+ message: `Saving pending OAuth session ${this.config.sessionId}`,
2827
+ sessionId: this.config.sessionId,
2828
+ serverId: this.config.serverId,
2726
2829
  timestamp: Date.now(),
2727
2830
  id: nanoid.nanoid()
2728
2831
  });
2729
2832
  await this.saveSession("pending");
2730
- if (this.serverId) {
2833
+ if (this.config.serverId) {
2731
2834
  this._onConnectionEvent.fire({
2732
2835
  type: "auth_required",
2733
- sessionId: this.sessionId,
2734
- serverId: this.serverId,
2836
+ sessionId: this.config.sessionId,
2837
+ serverId: this.config.serverId,
2735
2838
  authUrl,
2736
2839
  timestamp: Date.now()
2737
2840
  });
2738
- if (authUrl && this.onRedirect) {
2739
- this.onRedirect(authUrl);
2841
+ if (authUrl && this.config.onRedirect) {
2842
+ this.config.onRedirect(authUrl);
2740
2843
  }
2741
2844
  }
2742
2845
  throw new UnauthorizedError("OAuth authorization required");
@@ -2745,9 +2848,9 @@ var MCPClient = class {
2745
2848
  this.emitError(errorMessage, "connection");
2746
2849
  this.emitStateChange("FAILED");
2747
2850
  try {
2748
- const existingSession = await sessions.get(this.userId, this.sessionId);
2851
+ const existingSession = await this._store.get(this.config.userId, this.config.sessionId);
2749
2852
  if (!existingSession || existingSession.status !== "active") {
2750
- await sessions.delete(this.userId, this.sessionId);
2853
+ await this._store.delete(this.config.userId, this.config.sessionId);
2751
2854
  }
2752
2855
  } catch {
2753
2856
  }
@@ -2772,16 +2875,16 @@ var MCPClient = class {
2772
2875
  throw new Error(error);
2773
2876
  }
2774
2877
  if (state) {
2775
- const stateCheck = await this.oauthProvider.checkState(state);
2776
- if (!stateCheck.valid) {
2777
- const error = stateCheck.error || "Invalid OAuth state";
2778
- this.emitError(error, "auth");
2878
+ try {
2879
+ await this.oauthProvider.consumeState(state);
2880
+ } catch (error) {
2881
+ const msg = error instanceof Error ? error.message : "Invalid OAuth state";
2882
+ this.emitError(msg, "auth");
2779
2883
  this.emitStateChange("FAILED");
2780
- throw new Error(error);
2884
+ throw error;
2781
2885
  }
2782
- await this.oauthProvider.consumeState(state);
2783
2886
  }
2784
- const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
2887
+ const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
2785
2888
  let lastError;
2786
2889
  let tokensExchanged = false;
2787
2890
  let authenticatedStateEmitted = false;
@@ -2808,14 +2911,15 @@ var MCPClient = class {
2808
2911
  }
2809
2912
  }
2810
2913
  await this.client.connect(this.transport);
2811
- this.transportType = currentType;
2914
+ this._serverInfo = this.client.getServerVersion();
2915
+ this.config.transportType = currentType;
2812
2916
  this.emitStateChange("CONNECTED");
2813
2917
  this._onObservabilityEvent.fire({
2814
2918
  type: "mcp:client:session_saved",
2815
2919
  level: "info",
2816
- message: `Saving active session ${this.sessionId} (OAuth complete)`,
2817
- sessionId: this.sessionId,
2818
- serverId: this.serverId,
2920
+ message: `Saving active session ${this.config.sessionId} (OAuth complete)`,
2921
+ sessionId: this.config.sessionId,
2922
+ serverId: this.config.serverId,
2819
2923
  timestamp: Date.now(),
2820
2924
  id: nanoid.nanoid()
2821
2925
  });
@@ -2888,11 +2992,11 @@ var MCPClient = class {
2888
2992
  this.emitStateChange("DISCOVERING");
2889
2993
  try {
2890
2994
  const result = await this.fetchTools();
2891
- if (this.serverId) {
2995
+ if (this.config.serverId) {
2892
2996
  this._onConnectionEvent.fire({
2893
2997
  type: "tools_discovered",
2894
- sessionId: this.sessionId,
2895
- serverId: this.serverId,
2998
+ sessionId: this.config.sessionId,
2999
+ serverId: this.config.serverId,
2896
3000
  toolCount: result.tools.length,
2897
3001
  tools: result.tools,
2898
3002
  timestamp: Date.now()
@@ -2932,8 +3036,8 @@ var MCPClient = class {
2932
3036
  level: "info",
2933
3037
  message: `Tool ${toolName} called successfully`,
2934
3038
  displayMessage: `Called tool ${toolName}`,
2935
- sessionId: this.sessionId,
2936
- serverId: this.serverId,
3039
+ sessionId: this.config.sessionId,
3040
+ serverId: this.config.serverId,
2937
3041
  payload: {
2938
3042
  toolName,
2939
3043
  args: toolArgs
@@ -2949,8 +3053,8 @@ var MCPClient = class {
2949
3053
  level: "error",
2950
3054
  message: errorMessage,
2951
3055
  displayMessage: `Failed to call tool ${toolName}`,
2952
- sessionId: this.sessionId,
2953
- serverId: this.serverId,
3056
+ sessionId: this.config.sessionId,
3057
+ serverId: this.config.serverId,
2954
3058
  payload: {
2955
3059
  errorType: "tool_execution",
2956
3060
  error: errorMessage,
@@ -3098,8 +3202,8 @@ var MCPClient = class {
3098
3202
  type: "mcp:client:error",
3099
3203
  level: "warn",
3100
3204
  message: "Initialization failed during clearSession",
3101
- sessionId: this.sessionId,
3102
- serverId: this.serverId,
3205
+ sessionId: this.config.sessionId,
3206
+ serverId: this.config.serverId,
3103
3207
  payload: { error: String(error) },
3104
3208
  timestamp: Date.now(),
3105
3209
  id: nanoid.nanoid()
@@ -3108,7 +3212,7 @@ var MCPClient = class {
3108
3212
  if (this.oauthProvider) {
3109
3213
  await this.oauthProvider.invalidateCredentials("all");
3110
3214
  }
3111
- await sessions.delete(this.userId, this.sessionId);
3215
+ await this._store.delete(this.config.userId, this.config.sessionId);
3112
3216
  await this.disconnect();
3113
3217
  }
3114
3218
  /**
@@ -3142,19 +3246,19 @@ var MCPClient = class {
3142
3246
  }
3143
3247
  this.oauthProvider = null;
3144
3248
  this.transport = null;
3145
- if (this.serverId) {
3249
+ if (this.config.serverId) {
3146
3250
  this._onConnectionEvent.fire({
3147
3251
  type: "disconnected",
3148
- sessionId: this.sessionId,
3149
- serverId: this.serverId,
3252
+ sessionId: this.config.sessionId,
3253
+ serverId: this.config.serverId,
3150
3254
  timestamp: Date.now()
3151
3255
  });
3152
3256
  this._onObservabilityEvent.fire({
3153
3257
  type: "mcp:client:disconnect",
3154
3258
  level: "info",
3155
- message: `Disconnected from ${this.serverId}`,
3156
- sessionId: this.sessionId,
3157
- serverId: this.serverId,
3259
+ message: `Disconnected from ${this.config.serverId}`,
3260
+ sessionId: this.config.sessionId,
3261
+ serverId: this.config.serverId,
3158
3262
  payload: {},
3159
3263
  timestamp: Date.now(),
3160
3264
  id: nanoid.nanoid()
@@ -3179,42 +3283,52 @@ var MCPClient = class {
3179
3283
  * @returns Server URL or empty string if not set
3180
3284
  */
3181
3285
  getServerUrl() {
3182
- return this.serverUrl || "";
3286
+ return this.config.serverUrl || "";
3183
3287
  }
3184
3288
  /**
3185
3289
  * Gets the OAuth callback URL
3186
3290
  * @returns Callback URL or empty string if not set
3187
3291
  */
3188
3292
  getCallbackUrl() {
3189
- return this.callbackUrl || "";
3293
+ return this.config.callbackUrl || "";
3190
3294
  }
3191
3295
  /**
3192
3296
  * Gets the transport type being used
3193
3297
  * @returns Transport type (defaults to 'streamable-http')
3194
3298
  */
3195
3299
  getTransportType() {
3196
- return this.transportType || "streamable-http";
3300
+ return this.config.transportType || "streamable-http";
3301
+ }
3302
+ /**
3303
+ * Gets the full server metadata from the MCP initialize response.
3304
+ * Includes name, version, icons, title, description, and website URL.
3305
+ * Returns undefined if the client hasn't connected yet.
3306
+ */
3307
+ getServerInfo() {
3308
+ return this._serverInfo;
3197
3309
  }
3198
3310
  /**
3199
- * Gets the human-readable server name
3311
+ * Gets the human-readable server name.
3312
+ * Prefers the server's reported title/name from the initialize response,
3313
+ * falling back to the name provided at construction or session metadata.
3200
3314
  * @returns Server name or undefined
3201
3315
  */
3202
3316
  getServerName() {
3203
- return this.serverName;
3317
+ return this.config.serverName;
3204
3318
  }
3205
3319
  /**
3206
3320
  * Gets the server ID
3207
3321
  * @returns Server ID or undefined
3208
3322
  */
3209
3323
  getServerId() {
3210
- return this.serverId;
3324
+ return this.config.serverId;
3211
3325
  }
3212
3326
  /**
3213
3327
  * Gets the session ID
3214
3328
  * @returns Session ID
3215
3329
  */
3216
3330
  getSessionId() {
3217
- return this.sessionId;
3331
+ return this.config.sessionId;
3218
3332
  }
3219
3333
  };
3220
3334
 
@@ -3241,6 +3355,14 @@ var ToolPolicyGateway = class {
3241
3355
  getServerId() {
3242
3356
  return this.client.getServerId?.();
3243
3357
  }
3358
+ /**
3359
+ * Returns the full server metadata from the underlying client, if available.
3360
+ * Includes name, version, icons, title, description, and website URL.
3361
+ * Available only after the client has connected and completed initialization.
3362
+ */
3363
+ getServerInfo() {
3364
+ return this.client.getServerInfo?.();
3365
+ }
3244
3366
  /**
3245
3367
  * Returns the human-readable server name from the underlying client, if available.
3246
3368
  */
@@ -3369,6 +3491,7 @@ var MultiSessionClient = class {
3369
3491
  __publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
3370
3492
  __publicField(this, "userId");
3371
3493
  __publicField(this, "options");
3494
+ __publicField(this, "_store");
3372
3495
  this.userId = userId;
3373
3496
  this.options = {
3374
3497
  timeout: DEFAULT_TIMEOUT_MS,
@@ -3376,6 +3499,7 @@ var MultiSessionClient = class {
3376
3499
  retryDelay: DEFAULT_RETRY_DELAY_MS,
3377
3500
  ...options
3378
3501
  };
3502
+ this._store = options.onObservabilityEvent ? withDbObservability(options.sessionStore ?? sessions, options.onObservabilityEvent) : options.sessionStore ?? sessions;
3379
3503
  }
3380
3504
  // -----------------------------------------------------------------------
3381
3505
  // Public API
@@ -3459,9 +3583,9 @@ var MultiSessionClient = class {
3459
3583
  * to querying the storage backend via `sessions.list(userId)`.
3460
3584
  */
3461
3585
  async fetchActiveSessions() {
3462
- const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await sessions.list(this.userId);
3586
+ const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await this._store.list(this.userId);
3463
3587
  return sessionList.filter(
3464
- (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
3588
+ (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active" && s.enabled !== false
3465
3589
  );
3466
3590
  }
3467
3591
  /**
@@ -3534,8 +3658,13 @@ var MultiSessionClient = class {
3534
3658
  callbackUrl: session.callbackUrl,
3535
3659
  serverName: session.serverName,
3536
3660
  transportType: session.transportType,
3537
- headers: session.headers
3661
+ headers: session.headers,
3662
+ hasSession: true,
3663
+ sessionStore: this._store
3538
3664
  });
3665
+ if (this.options.onObservabilityEvent) {
3666
+ client.onObservabilityEvent(this.options.onObservabilityEvent);
3667
+ }
3539
3668
  const timeoutMs = this.options.timeout;
3540
3669
  let timeoutTimer;
3541
3670
  const timeoutPromise = new Promise((_, reject) => {
@@ -3594,130 +3723,188 @@ function isConnectionEvent(event) {
3594
3723
  }
3595
3724
 
3596
3725
  // src/server/handlers/sse-handler.ts
3597
- var DEFAULT_HEARTBEAT_INTERVAL = 3e4;
3726
+ var DEFAULT_HEARTBEAT_MS = 3e4;
3598
3727
  function normalizeHeaders(headers) {
3599
3728
  if (!headers || typeof headers !== "object") return void 0;
3600
- const entries = Object.entries(headers).map(([key, value]) => [key.trim(), String(value).trim()]).filter(([key, value]) => key.length > 0 && value.length > 0);
3729
+ const entries = Object.entries(headers).map(([k, v]) => [k.trim(), String(v).trim()]).filter(([k, v]) => k.length > 0 && v.length > 0);
3601
3730
  return entries.length > 0 ? Object.fromEntries(entries) : void 0;
3602
3731
  }
3732
+ function validatingStateEvent(sessionId, session) {
3733
+ return {
3734
+ type: "state_changed",
3735
+ sessionId,
3736
+ serverId: session.serverId ?? "unknown",
3737
+ serverName: session.serverName ?? "Unknown",
3738
+ serverUrl: session.serverUrl,
3739
+ state: "VALIDATING",
3740
+ previousState: "DISCONNECTED",
3741
+ timestamp: Date.now()
3742
+ };
3743
+ }
3744
+ function connectionErrorEvent(sessionId, serverId, error, errorType) {
3745
+ return {
3746
+ type: "error",
3747
+ sessionId,
3748
+ serverId: serverId ?? "unknown",
3749
+ error: error instanceof Error ? error.message : "Connection failed",
3750
+ errorType,
3751
+ timestamp: Date.now()
3752
+ };
3753
+ }
3603
3754
  var SSEConnectionManager = class {
3755
+ // -----------------------------------------------------------------------
3756
+ // Constructor
3757
+ // -----------------------------------------------------------------------
3758
+ /**
3759
+ * @param options - Handler configuration (userId, auth, metadata, heartbeat, observability).
3760
+ * @param sendEvent - Callback that writes a typed event onto the SSE stream.
3761
+ */
3604
3762
  constructor(options, sendEvent) {
3605
3763
  this.options = options;
3606
3764
  this.sendEvent = sendEvent;
3765
+ // -----------------------------------------------------------------------
3766
+ // State
3767
+ // -----------------------------------------------------------------------
3607
3768
  __publicField(this, "userId");
3769
+ /** Active MCP transports keyed by sessionId. */
3608
3770
  __publicField(this, "clients", /* @__PURE__ */ new Map());
3771
+ /** Instrumented session store — always wraps `sessions` with DB observability. */
3772
+ __publicField(this, "observedStore");
3609
3773
  __publicField(this, "heartbeatTimer");
3610
- __publicField(this, "isActive", true);
3774
+ __publicField(this, "active", true);
3611
3775
  this.userId = options.userId;
3776
+ this.observedStore = withDbObservability(sessions, (event) => this.sendEvent(event));
3612
3777
  this.startHeartbeat();
3613
3778
  }
3779
+ // -----------------------------------------------------------------------
3780
+ // Public API
3781
+ // -----------------------------------------------------------------------
3614
3782
  /**
3615
- * Get resolved client metadata (dynamic > static > defaults)
3616
- */
3617
- async getResolvedClientMetadata(request) {
3618
- let metadata = {};
3619
- if (this.options.clientDefaults) {
3620
- metadata = { ...this.options.clientDefaults };
3621
- }
3622
- if (this.options.getClientMetadata) {
3623
- const dynamicMetadata = await this.options.getClientMetadata(request);
3624
- metadata = { ...metadata, ...dynamicMetadata };
3625
- }
3626
- return metadata;
3627
- }
3628
- /**
3629
- * Start heartbeat to keep connection alive
3630
- */
3631
- startHeartbeat() {
3632
- const interval = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL;
3633
- this.heartbeatTimer = setInterval(() => {
3634
- if (this.isActive) {
3635
- this.sendEvent({
3636
- level: "debug",
3637
- message: "heartbeat",
3638
- timestamp: Date.now()
3639
- });
3640
- }
3641
- }, interval);
3642
- }
3643
- /**
3644
- * Handle incoming RPC requests
3645
- * Returns the RPC response directly for immediate HTTP response (bypassing SSE latency)
3783
+ * Dispatches an incoming RPC request, emits timing observability,
3784
+ * and returns the response.
3785
+ *
3786
+ * Emits `rpc:start` before dispatch and `rpc:end` on completion
3787
+ * (success or error), each carrying the method name, sessionId, and
3788
+ * duration. All events flow through the unified `onObservability`
3789
+ * and the SSE stream.
3790
+ *
3791
+ * @param request - The deserialized RPC envelope.
3792
+ * @returns The RPC response (success or error).
3646
3793
  */
3647
3794
  async handleRequest(request) {
3795
+ const method = request.method;
3796
+ const sessionId = request.params?.sessionId;
3797
+ const t0 = performance.now();
3798
+ this.sendEvent({
3799
+ type: "rpc:start",
3800
+ level: "debug",
3801
+ message: method,
3802
+ sessionId,
3803
+ timestamp: Date.now()
3804
+ });
3648
3805
  try {
3649
- let result;
3650
- switch (request.method) {
3651
- case "listSessions":
3652
- result = await this.listSessions();
3653
- break;
3654
- case "connect":
3655
- result = await this.connect(request.params);
3656
- break;
3657
- case "reconnect":
3658
- result = await this.reconnect(request.params);
3659
- break;
3660
- case "disconnect":
3661
- result = await this.disconnect(request.params);
3662
- break;
3663
- case "listTools":
3664
- result = await this.listTools(request.params);
3665
- break;
3666
- case "setToolPolicy":
3667
- result = await this.setToolPolicy(request.params);
3668
- break;
3669
- case "getToolPolicy":
3670
- result = await this.getToolPolicy(request.params);
3671
- break;
3672
- case "callTool":
3673
- result = await this.callTool(request.params);
3674
- break;
3675
- case "getSession":
3676
- result = await this.getSession(request.params);
3677
- break;
3678
- case "finishAuth":
3679
- result = await this.finishAuth(request.params);
3680
- break;
3681
- case "listPrompts":
3682
- result = await this.listPrompts(request.params);
3683
- break;
3684
- case "getPrompt":
3685
- result = await this.getPrompt(request.params);
3686
- break;
3687
- case "listResources":
3688
- result = await this.listResources(request.params);
3689
- break;
3690
- case "readResource":
3691
- result = await this.readResource(request.params);
3692
- break;
3693
- default:
3694
- throw new Error(`Unknown method: ${request.method}`);
3695
- }
3696
- const response = {
3697
- id: request.id,
3698
- result
3699
- };
3806
+ const result = await this.dispatchImpl(request);
3807
+ this.sendEvent({
3808
+ type: "rpc:end",
3809
+ level: "debug",
3810
+ message: method,
3811
+ sessionId,
3812
+ payload: { durationMs: performance.now() - t0 },
3813
+ timestamp: Date.now()
3814
+ });
3815
+ const response = { id: request.id, result };
3700
3816
  this.sendEvent(response);
3701
3817
  return response;
3702
3818
  } catch (error) {
3703
- const errorResponse = {
3819
+ this.sendEvent({
3820
+ type: "rpc:end",
3821
+ level: "error",
3822
+ message: method,
3823
+ sessionId,
3824
+ payload: { durationMs: performance.now() - t0, error: String(error) },
3825
+ timestamp: Date.now()
3826
+ });
3827
+ const response = {
3704
3828
  id: request.id,
3705
3829
  error: {
3706
3830
  code: RpcErrorCodes.EXECUTION_ERROR,
3707
3831
  message: error instanceof Error ? error.message : "Unknown error"
3708
3832
  }
3709
3833
  };
3710
- this.sendEvent(errorResponse);
3711
- return errorResponse;
3834
+ this.sendEvent(response);
3835
+ return response;
3712
3836
  }
3713
3837
  }
3714
3838
  /**
3715
- * Get all sessions for the current userId
3839
+ * Tears down all active MCP transports and stops the heartbeat timer.
3840
+ *
3841
+ * Disconnects are issued in parallel across all sessions. After calling
3842
+ * this method the manager instance should be discarded.
3843
+ */
3844
+ async dispose() {
3845
+ this.active = false;
3846
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
3847
+ await Promise.all(
3848
+ Array.from(this.clients.values()).map((c) => c.disconnect())
3849
+ );
3850
+ this.clients.clear();
3851
+ }
3852
+ // -----------------------------------------------------------------------
3853
+ // RPC Dispatch (raw — called by handleRequest which adds timing)
3854
+ // -----------------------------------------------------------------------
3855
+ /**
3856
+ * Routes an RPC method name to the appropriate private handler.
3857
+ *
3858
+ * @throws {Error} When the method name is unrecognized.
3859
+ */
3860
+ async dispatchImpl(request) {
3861
+ switch (request.method) {
3862
+ case "listSessions":
3863
+ return this.listSessions();
3864
+ case "connect":
3865
+ return this.connect(request.params);
3866
+ case "reconnect":
3867
+ return this.reconnect(request.params);
3868
+ case "disconnect":
3869
+ return this.disconnect(request.params);
3870
+ case "listTools":
3871
+ return this.listTools(request.params);
3872
+ case "setToolPolicy":
3873
+ return this.setToolPolicy(request.params);
3874
+ case "getToolPolicy":
3875
+ return this.getToolPolicy(request.params);
3876
+ case "updateSession":
3877
+ return this.updateSession(request.params);
3878
+ case "callTool":
3879
+ return this.callTool(request.params);
3880
+ case "getSession":
3881
+ return this.getSession(request.params);
3882
+ case "finishAuth":
3883
+ return this.finishAuth(request.params);
3884
+ case "listPrompts":
3885
+ return this.listPrompts(request.params);
3886
+ case "getPrompt":
3887
+ return this.getPrompt(request.params);
3888
+ case "listResources":
3889
+ return this.listResources(request.params);
3890
+ case "readResource":
3891
+ return this.readResource(request.params);
3892
+ default:
3893
+ throw new Error(`Unknown RPC method: ${request.method}`);
3894
+ }
3895
+ }
3896
+ // -----------------------------------------------------------------------
3897
+ // Session Query
3898
+ // -----------------------------------------------------------------------
3899
+ /**
3900
+ * Lists all sessions owned by the current user.
3901
+ *
3902
+ * Returns a lightweight view — session metadata only, no credential fields.
3716
3903
  */
3717
3904
  async listSessions() {
3718
- const sessionList = await sessions.list(this.userId);
3905
+ const all = await sessions.list(this.userId);
3719
3906
  return {
3720
- sessions: sessionList.map((s) => ({
3907
+ sessions: all.map((s) => ({
3721
3908
  sessionId: s.sessionId,
3722
3909
  serverId: s.serverId,
3723
3910
  serverName: s.serverName,
@@ -3726,215 +3913,203 @@ var SSEConnectionManager = class {
3726
3913
  createdAt: s.createdAt,
3727
3914
  updatedAt: s.updatedAt ?? s.createdAt,
3728
3915
  status: s.status ?? "pending",
3729
- toolPolicy: s.toolPolicy
3916
+ toolPolicy: s.toolPolicy,
3917
+ enabled: s.enabled ?? true
3730
3918
  }))
3731
3919
  };
3732
3920
  }
3733
3921
  /**
3734
- * Connect to an MCP server
3922
+ * Restores and validates a previously persisted session.
3923
+ *
3924
+ * Loads the full session row (including credentials) from storage, creates
3925
+ * a new MCP transport, connects to the remote server, and emits the
3926
+ * discovered (policy-filtered) tool list via SSE.
3735
3927
  */
3736
- async connect(params) {
3737
- const { serverName, serverUrl, callbackUrl, transportType, clientId, clientSecret } = params;
3738
- const headers = normalizeHeaders(params.headers);
3739
- const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : generateServerId();
3740
- const existingSessions = await sessions.list(this.userId);
3741
- const duplicate = existingSessions.find(
3742
- (s) => s.serverId === serverId || s.serverUrl === serverUrl
3743
- );
3744
- if (duplicate) {
3745
- if (duplicate.status === "pending") {
3746
- await this.getSession({ sessionId: duplicate.sessionId });
3747
- return {
3748
- sessionId: duplicate.sessionId,
3749
- success: true
3750
- };
3751
- }
3752
- throw new Error(`Connection already exists for server: ${duplicate.serverUrl || duplicate.serverId} (${duplicate.serverName})`);
3753
- }
3754
- const sessionId = await sessions.generateSessionId();
3928
+ async getSession(params) {
3929
+ const session = await this.requireSession(params.sessionId);
3930
+ this.sendEvent(validatingStateEvent(params.sessionId, session));
3755
3931
  try {
3756
- const clientMetadata = await this.getResolvedClientMetadata();
3757
- const client = new MCPClient({
3758
- userId: this.userId,
3759
- sessionId,
3760
- serverId,
3761
- serverName,
3762
- serverUrl,
3763
- callbackUrl,
3764
- transportType,
3765
- headers,
3766
- clientId,
3767
- clientSecret,
3768
- ...clientMetadata
3769
- // Spread client metadata (clientName, clientUri, logoUri, policyUri)
3770
- });
3771
- this.clients.set(sessionId, client);
3772
- client.onConnectionEvent((event) => {
3773
- this.emitConnectionEvent(event);
3774
- });
3775
- client.onObservabilityEvent((event) => {
3776
- this.sendEvent(event);
3777
- });
3932
+ const client = this.restoreClient(session);
3933
+ this.attachClientEvents(client);
3778
3934
  await client.connect();
3779
- await this.listPolicyFilteredTools(sessionId);
3780
- return {
3781
- sessionId,
3782
- success: true
3783
- };
3935
+ this.clients.set(params.sessionId, client);
3936
+ const { result } = await this.listPolicyFilteredTools(params.sessionId);
3937
+ return { success: true, toolCount: result.tools.length };
3784
3938
  } catch (error) {
3785
- if (error instanceof UnauthorizedError) {
3786
- this.clients.delete(sessionId);
3787
- return {
3788
- sessionId,
3789
- success: true
3790
- };
3791
- }
3792
- this.emitConnectionEvent({
3793
- type: "error",
3794
- sessionId,
3795
- serverId,
3796
- error: error instanceof Error ? error.message : "Connection failed",
3797
- errorType: "connection",
3798
- timestamp: Date.now()
3799
- });
3800
- this.clients.delete(sessionId);
3939
+ this.sendEvent(connectionErrorEvent(params.sessionId, session.serverId, error, "validation"));
3801
3940
  throw error;
3802
3941
  }
3803
3942
  }
3943
+ // -----------------------------------------------------------------------
3944
+ // Connection Lifecycle
3945
+ // -----------------------------------------------------------------------
3804
3946
  /**
3805
- * Reconnect to an MCP server — tears down the active client transport/connection
3806
- * and creates a fresh connection while reusing the existing session credentials in a single RPC call.
3947
+ * Initiates a connection to a new MCP server.
3948
+ *
3949
+ * If a session for the same `serverId` or `serverUrl` already exists and
3950
+ * is still in a `pending` (OAuth) state, the existing session is resumed
3951
+ * instead of creating a duplicate.
3952
+ *
3953
+ * `UnauthorizedError` is treated as a pending-auth state and returned as
3954
+ * a successful result (the client will then redirect to the auth URL).
3807
3955
  */
3808
- async reconnect(params) {
3809
- const { serverId: rawServerId, serverName, serverUrl, callbackUrl, transportType, clientId, clientSecret } = params;
3956
+ async connect(params) {
3810
3957
  const headers = normalizeHeaders(params.headers);
3811
- const serverId = rawServerId && rawServerId.length <= 12 ? rawServerId : generateServerId();
3812
- const existingSessions = await sessions.list(this.userId);
3813
- const duplicate = existingSessions.find(
3814
- (s) => s.serverId === serverId || s.serverUrl === serverUrl
3815
- );
3816
- const sessionId = duplicate ? duplicate.sessionId : await sessions.generateSessionId();
3817
- if (duplicate) {
3818
- const existingClient = this.clients.get(duplicate.sessionId);
3819
- if (existingClient) {
3820
- await existingClient.disconnect();
3821
- this.clients.delete(duplicate.sessionId);
3958
+ const serverId = this.normalizeServerId(params.serverId);
3959
+ const existing = await this.findExistingSession(serverId, params.serverUrl);
3960
+ if (existing) {
3961
+ if (existing.status === "pending") {
3962
+ return this.getSession({ sessionId: existing.sessionId }).then(() => ({
3963
+ sessionId: existing.sessionId,
3964
+ success: true
3965
+ }));
3822
3966
  }
3967
+ throw new Error(
3968
+ `Connection already exists for server: ${existing.serverUrl ?? existing.serverId} (${existing.serverName})`
3969
+ );
3823
3970
  }
3824
- try {
3825
- const clientMetadata = await this.getResolvedClientMetadata();
3826
- const client = new MCPClient({
3827
- userId: this.userId,
3828
- sessionId,
3829
- serverId,
3830
- serverName,
3831
- serverUrl,
3832
- callbackUrl,
3833
- transportType,
3834
- headers,
3835
- clientId,
3836
- clientSecret,
3837
- ...clientMetadata
3838
- });
3839
- this.clients.set(sessionId, client);
3840
- client.onConnectionEvent((event) => {
3841
- this.emitConnectionEvent(event);
3842
- });
3843
- client.onObservabilityEvent((event) => {
3844
- this.sendEvent(event);
3845
- });
3846
- await client.connect();
3847
- await this.listPolicyFilteredTools(sessionId);
3848
- return { sessionId, success: true };
3849
- } catch (error) {
3850
- if (error instanceof UnauthorizedError) {
3851
- this.clients.delete(sessionId);
3852
- return { sessionId, success: true };
3971
+ const sessionId = await sessions.generateSessionId();
3972
+ const metadata = await this.getResolvedClientMetadata();
3973
+ const client = new MCPClient({
3974
+ userId: this.userId,
3975
+ sessionId,
3976
+ serverId,
3977
+ serverName: params.serverName,
3978
+ serverUrl: params.serverUrl,
3979
+ callbackUrl: params.callbackUrl,
3980
+ transportType: params.transportType,
3981
+ headers,
3982
+ sessionStore: this.observedStore,
3983
+ ...metadata
3984
+ });
3985
+ this.cacheClient(sessionId, client);
3986
+ return this.connectAndDiscover(client, sessionId, serverId);
3987
+ }
3988
+ /**
3989
+ * Reconnects to an MCP server by tearing down the active transport if one
3990
+ * exists and instantiating a fresh connection, reusing the existing session
3991
+ * (and its stored credentials / DCR client info) from the database.
3992
+ */
3993
+ async reconnect(params) {
3994
+ const headers = normalizeHeaders(params.headers);
3995
+ const serverId = this.normalizeServerId(params.serverId);
3996
+ const existing = await this.findExistingSession(serverId, params.serverUrl);
3997
+ const sessionId = existing ? existing.sessionId : await sessions.generateSessionId();
3998
+ if (existing) {
3999
+ const staleClient = this.clients.get(existing.sessionId);
4000
+ if (staleClient) {
4001
+ await staleClient.disconnect();
4002
+ this.clients.delete(existing.sessionId);
3853
4003
  }
3854
- this.emitConnectionEvent({
3855
- type: "error",
3856
- sessionId,
3857
- serverId,
3858
- error: error instanceof Error ? error.message : "Connection failed",
3859
- errorType: "connection",
3860
- timestamp: Date.now()
3861
- });
3862
- this.clients.delete(sessionId);
3863
- throw error;
3864
4004
  }
4005
+ const metadata = await this.getResolvedClientMetadata();
4006
+ const client = new MCPClient({
4007
+ userId: this.userId,
4008
+ sessionId,
4009
+ serverId,
4010
+ serverName: params.serverName,
4011
+ serverUrl: params.serverUrl,
4012
+ callbackUrl: params.callbackUrl,
4013
+ transportType: params.transportType,
4014
+ headers,
4015
+ sessionStore: this.observedStore,
4016
+ ...metadata
4017
+ });
4018
+ this.cacheClient(sessionId, client);
4019
+ return this.connectAndDiscover(client, sessionId, serverId);
3865
4020
  }
3866
4021
  /**
3867
- * Disconnect from an MCP server
4022
+ * Disconnects from an MCP server.
4023
+ *
4024
+ * If an active in-memory transport exists it delegates to `clearSession()`
4025
+ * (which sends the server an HTTP DELETE per the Streamable spec). If no
4026
+ * active transport is available (orphaned session from a failed OAuth flow),
4027
+ * the session row is removed directly from storage.
3868
4028
  */
3869
4029
  async disconnect(params) {
3870
- const { sessionId } = params;
3871
- const client = this.clients.get(sessionId);
4030
+ const client = this.clients.get(params.sessionId);
3872
4031
  if (client) {
3873
4032
  await client.clearSession();
3874
- this.clients.delete(sessionId);
4033
+ this.clients.delete(params.sessionId);
3875
4034
  } else {
3876
- await sessions.delete(this.userId, sessionId);
4035
+ await sessions.delete(this.userId, params.sessionId);
3877
4036
  }
3878
4037
  return { success: true };
3879
4038
  }
4039
+ // -----------------------------------------------------------------------
4040
+ // OAuth
4041
+ // -----------------------------------------------------------------------
3880
4042
  /**
3881
- * Get an existing client or create and connect a new one for the session.
4043
+ * Completes the OAuth 2.1 authorization code flow for a pending session.
4044
+ *
4045
+ * Loads the stored session (with credentials), creates a fresh `MCPClient`,
4046
+ * and calls `finishAuth` inside a {@link runWithCodeVerifierState} context
4047
+ * so the PKCE code verifier is available without a DB read.
4048
+ *
4049
+ * The session's stored `transportType` is intentionally **omitted** from the
4050
+ * client config — this lets `MCPClient` auto-negotiate (try Streamable HTTP
4051
+ * first, fall back to SSE), which is required for servers like Neon that
4052
+ * only support SSE transport.
3882
4053
  */
3883
- async getOrCreateClient(sessionId) {
3884
- const existing = this.clients.get(sessionId);
3885
- if (existing) {
3886
- return existing;
3887
- }
3888
- const session = await sessions.get(this.userId, sessionId);
3889
- if (!session) {
3890
- throw new Error("Session not found");
4054
+ async finishAuth(params) {
4055
+ const parsed = parseOAuthState(params.state);
4056
+ const sessionId = parsed?.sessionId ?? params.state;
4057
+ const session = await this.requireSession(sessionId);
4058
+ try {
4059
+ const clientId = session.clientId ?? void 0;
4060
+ const clientSecret = session.clientInformation?.client_secret ?? void 0;
4061
+ const client = new MCPClient({
4062
+ userId: this.userId,
4063
+ sessionId,
4064
+ serverId: session.serverId,
4065
+ serverName: session.serverName,
4066
+ serverUrl: session.serverUrl,
4067
+ callbackUrl: session.callbackUrl,
4068
+ headers: session.headers,
4069
+ clientId,
4070
+ clientSecret,
4071
+ hasSession: true,
4072
+ cachedCredentials: { tokens: session.tokens ?? void 0 },
4073
+ sessionStore: this.observedStore
4074
+ });
4075
+ this.attachClientEvents(client);
4076
+ await runWithCodeVerifierState(
4077
+ session.codeVerifier ?? "",
4078
+ "S256",
4079
+ () => client.finishAuth(params.code, params.state)
4080
+ );
4081
+ this.clients.set(sessionId, client);
4082
+ const { result } = await this.listPolicyFilteredTools(sessionId);
4083
+ return { success: true, toolCount: result.tools.length };
4084
+ } catch (error) {
4085
+ this.sendEvent(connectionErrorEvent(sessionId, session.serverId, error, "auth"));
4086
+ throw error;
3891
4087
  }
3892
- const client = new MCPClient({
3893
- userId: this.userId,
3894
- sessionId,
3895
- // These fields are optional in MCPClient, but when rehydrating a known
3896
- // stored session on the server we pass them explicitly to preserve the
3897
- // original transport/connection metadata instead of relying on lazy
3898
- // reloading during initialize().
3899
- serverId: session.serverId,
3900
- serverName: session.serverName,
3901
- serverUrl: session.serverUrl,
3902
- callbackUrl: session.callbackUrl,
3903
- transportType: session.transportType,
3904
- headers: session.headers
3905
- });
3906
- client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3907
- client.onObservabilityEvent((event) => this.sendEvent(event));
3908
- await client.connect();
3909
- this.clients.set(sessionId, client);
3910
- return client;
3911
4088
  }
4089
+ // -----------------------------------------------------------------------
4090
+ // Tool Discovery & Policy
4091
+ // -----------------------------------------------------------------------
3912
4092
  /**
3913
- * Fetches all tools from the remote MCP server and emits a `tools_discovered` event.
4093
+ * Fetches the complete tool list from the remote MCP server and emits a
4094
+ * `tools_discovered` SSE event with two lists:
3914
4095
  *
3915
- * Two lists are always published together:
3916
- * - `tools` policy-filtered list that agents are allowed to call.
3917
- * - `allTools` — the complete, unfiltered list used by the management UI so
3918
- * that blocked tools still appear as checkboxes in the dialog.
4096
+ * - `tools` — policy-filtered (what agents are allowed to call)
4097
+ * - `allTools` complete, unfiltered (used by the management UI)
3919
4098
  *
3920
- * `fetchTools()` is called first (populates the in-memory cache), then
3921
- * `gateway.listTools()` re-uses that cache internally — so only one remote
3922
- * network round-trip is made regardless of how many callers follow.
4099
+ * Only a single network round-trip is made: `fetchTools()` populates the
4100
+ * in-memory cache, then `gateway.listTools()` reuses that cache.
3923
4101
  *
3924
- * @param sessionId - The session whose tools should be discovered.
3925
- * @returns The session record and the policy-filtered tool list.
3926
- * @throws {Error} When the session does not exist in the store.
4102
+ * @param sessionId - The session to discover tools for.
4103
+ * @returns The session record and the filtered tool result.
4104
+ * @throws {Error} If the session does not exist in storage.
3927
4105
  */
3928
4106
  async listPolicyFilteredTools(sessionId) {
3929
- const session = await sessions.get(this.userId, sessionId);
3930
- if (!session) {
3931
- throw new Error("Session not found");
3932
- }
4107
+ const session = await this.requireSession(sessionId);
3933
4108
  const client = await this.getOrCreateClient(sessionId);
3934
4109
  const allTools = await client.fetchTools().catch(() => ({ tools: [] }));
3935
4110
  const gateway = createToolPolicyGateway(this.userId, sessionId, client);
3936
4111
  const result = await gateway.listTools();
3937
- this.emitConnectionEvent({
4112
+ this.sendEvent({
3938
4113
  type: "tools_discovered",
3939
4114
  sessionId,
3940
4115
  serverId: session.serverId ?? "unknown",
@@ -3946,254 +4121,254 @@ var SSEConnectionManager = class {
3946
4121
  return { session, result };
3947
4122
  }
3948
4123
  /**
3949
- * Returns the policy-filtered tool list for a session (agent-facing).
3950
- * Internally re-uses `listPolicyFilteredTools` which also emits a
3951
- * `tools_discovered` SSE event to keep client state up to date.
4124
+ * Returns the policy-filtered tool list for a session.
4125
+ *
4126
+ * Delegates to {@link listPolicyFilteredTools}, which also emits a
4127
+ * `tools_discovered` SSE event to keep the client state current.
3952
4128
  */
3953
4129
  async listTools(params) {
3954
- const { sessionId } = params;
3955
- const { result } = await this.listPolicyFilteredTools(sessionId);
4130
+ const { result } = await this.listPolicyFilteredTools(params.sessionId);
3956
4131
  return { tools: result.tools };
3957
4132
  }
3958
4133
  /**
3959
- * Get all raw tools with effective access state for management UI.
4134
+ * Returns all raw tools annotated with their effective policy state
4135
+ * for display in the management UI.
3960
4136
  */
3961
4137
  async getToolPolicy(params) {
3962
- const { sessionId } = params;
3963
- const session = await sessions.get(this.userId, sessionId);
3964
- if (!session) {
3965
- throw new Error("Session not found");
3966
- }
3967
- const client = await this.getOrCreateClient(sessionId);
4138
+ const session = await this.requireSession(params.sessionId);
4139
+ const client = await this.getOrCreateClient(params.sessionId);
3968
4140
  const allTools = await client.fetchTools();
3969
- const toolPolicy = session.toolPolicy ?? {
4141
+ const policy = session.toolPolicy ?? {
3970
4142
  mode: "all",
3971
4143
  toolIds: [],
3972
4144
  updatedAt: session.updatedAt ?? session.createdAt
3973
4145
  };
3974
4146
  const serverId = session.serverId ?? "unknown";
3975
- const tools = allTools.tools.map((tool) => ({
3976
- ...tool,
3977
- toolId: createToolId(serverId, tool.name),
3978
- allowed: isToolAllowed(toolPolicy, tool.name, session.serverId)
4147
+ const tools = allTools.tools.map((t) => ({
4148
+ ...t,
4149
+ toolId: createToolId(serverId, t.name),
4150
+ allowed: isToolAllowed(policy, t.name, session.serverId)
3979
4151
  }));
3980
4152
  return {
3981
- toolPolicy,
4153
+ toolPolicy: policy,
3982
4154
  tools,
3983
4155
  toolCount: tools.length,
3984
- allowedToolCount: tools.filter((tool) => tool.allowed).length
4156
+ allowedToolCount: tools.filter((t) => t.allowed).length
3985
4157
  };
3986
4158
  }
3987
4159
  /**
3988
- * Persists a new tool access policy for a session and broadcasts the updated
3989
- * filtered tool list to all connected browser clients via a `tools_discovered` event.
3990
- *
3991
- * Both `tools` (policy-filtered) and `allTools` (complete list) are emitted
3992
- * so the management UI can immediately reflect the new checkbox states without
3993
- * an additional round-trip to the server.
4160
+ * Persists a new tool access policy and broadcasts the updated filtered
4161
+ * tool list via a `tools_discovered` SSE event.
3994
4162
  *
3995
- * @param params - Session ID and the new `{ mode, toolIds }` policy to apply.
3996
- * @throws {Error} When the session does not exist or the policy references unknown tool IDs.
4163
+ * @throws {Error} If the session does not exist or the policy references
4164
+ * tool IDs that don't match any known tool.
3997
4165
  */
3998
4166
  async setToolPolicy(params) {
3999
- const { sessionId } = params;
4000
- const session = await sessions.get(this.userId, sessionId);
4001
- if (!session) {
4002
- throw new Error("Session not found");
4003
- }
4004
- const client = await this.getOrCreateClient(sessionId);
4167
+ const session = await this.requireSession(params.sessionId);
4168
+ const client = await this.getOrCreateClient(params.sessionId);
4005
4169
  const allTools = await client.fetchTools();
4006
- const toolPolicy = normalizeToolPolicyForUpdate(params.toolPolicy);
4007
- validateToolPolicyAgainstTools(toolPolicy, allTools.tools, session.serverId);
4008
- await sessions.update(this.userId, sessionId, { toolPolicy });
4009
- const filteredTools = createToolPolicyGateway(this.userId, sessionId, client).filterTools({ ...session, toolPolicy }, allTools.tools);
4010
- this.emitConnectionEvent({
4170
+ const policy = normalizeToolPolicyForUpdate(params.toolPolicy);
4171
+ validateToolPolicyAgainstTools(policy, allTools.tools, session.serverId);
4172
+ await sessions.update(this.userId, params.sessionId, { toolPolicy: policy });
4173
+ const filtered = createToolPolicyGateway(
4174
+ this.userId,
4175
+ params.sessionId,
4176
+ client
4177
+ ).filterTools({ ...session, toolPolicy: policy }, allTools.tools);
4178
+ this.sendEvent({
4011
4179
  type: "tools_discovered",
4012
- sessionId,
4180
+ sessionId: params.sessionId,
4013
4181
  serverId: session.serverId ?? "unknown",
4014
- toolCount: filteredTools.length,
4015
- tools: filteredTools,
4182
+ toolCount: filtered.length,
4183
+ tools: filtered,
4016
4184
  allTools: allTools.tools,
4017
4185
  timestamp: Date.now()
4018
4186
  });
4019
- return {
4020
- success: true,
4021
- toolPolicy,
4022
- tools: filteredTools,
4023
- toolCount: filteredTools.length
4024
- };
4187
+ return { success: true, toolPolicy: policy, tools: filtered, toolCount: filtered.length };
4025
4188
  }
4026
4189
  /**
4027
- * Proxies a tool invocation to the remote MCP server.
4028
- * Resolves the client for the given session and delegates to the tool router.
4029
- */
4030
- async callTool(params) {
4031
- const { sessionId, toolName, toolArgs } = params;
4032
- const client = await this.getOrCreateClient(sessionId);
4033
- const result = await createToolPolicyGateway(this.userId, sessionId, client).callTool(toolName, toolArgs);
4034
- const meta = result._meta || {};
4035
- return {
4036
- ...result,
4037
- _meta: {
4038
- ...meta,
4039
- sessionId
4040
- }
4041
- };
4042
- }
4043
- /**
4044
- * Restore and validate an existing session
4190
+ * Enables or disables a session for agent tool discovery.
4191
+ *
4192
+ * Disabled sessions retain their OAuth tokens and connection metadata
4193
+ * but are hidden from `MultiSessionClient.connect()` and blocked from
4194
+ * RPC tool access. Re-enabling does not require re-authentication.
4195
+ *
4196
+ * @param params - `{ sessionId, enabled: boolean }`
4197
+ * @returns `{ success: true }`
4198
+ * @throws {Error} If the session does not exist.
4045
4199
  */
4046
- async getSession(params) {
4047
- const { sessionId } = params;
4048
- const session = await sessions.get(this.userId, sessionId);
4049
- if (!session) {
4050
- throw new Error("Session not found");
4051
- }
4052
- this.emitConnectionEvent({
4053
- type: "state_changed",
4054
- sessionId,
4055
- serverId: session.serverId ?? "unknown",
4056
- serverName: session.serverName ?? "Unknown",
4057
- serverUrl: session.serverUrl,
4058
- state: "VALIDATING",
4059
- previousState: "DISCONNECTED",
4060
- timestamp: Date.now()
4061
- });
4062
- try {
4063
- const clientMetadata = await this.getResolvedClientMetadata();
4064
- const client = new MCPClient({
4065
- userId: this.userId,
4066
- sessionId,
4067
- // These fields are optional in MCPClient, but when rehydrating a known
4068
- // stored session on the server we pass them explicitly to preserve the
4069
- // original transport/connection metadata instead of relying on lazy
4070
- // reloading during initialize().
4071
- serverId: session.serverId,
4072
- serverName: session.serverName,
4073
- serverUrl: session.serverUrl,
4074
- callbackUrl: session.callbackUrl,
4075
- transportType: session.transportType,
4076
- headers: session.headers,
4077
- ...clientMetadata
4078
- });
4079
- client.onConnectionEvent((event) => this.emitConnectionEvent(event));
4080
- client.onObservabilityEvent((event) => this.sendEvent(event));
4081
- await client.connect();
4082
- this.clients.set(sessionId, client);
4083
- const { result: tools } = await this.listPolicyFilteredTools(sessionId);
4084
- return { success: true, toolCount: tools.tools.length };
4085
- } catch (error) {
4086
- this.emitConnectionEvent({
4087
- type: "error",
4088
- sessionId,
4089
- serverId: session.serverId ?? "unknown",
4090
- error: error instanceof Error ? error.message : "Validation failed",
4091
- errorType: "validation",
4092
- timestamp: Date.now()
4093
- });
4094
- throw error;
4095
- }
4200
+ async updateSession(params) {
4201
+ await this.requireSession(params.sessionId);
4202
+ await sessions.update(this.userId, params.sessionId, { enabled: params.enabled });
4203
+ return { success: true };
4096
4204
  }
4205
+ // -----------------------------------------------------------------------
4206
+ // Tool Execution
4207
+ // -----------------------------------------------------------------------
4097
4208
  /**
4098
- * Complete OAuth authorization flow
4209
+ * Proxies a tool invocation to the remote MCP server.
4210
+ *
4211
+ * Resolves (or creates) the transport for the given session, runs the call
4212
+ * through the tool-policy gateway, and injects `sessionId` into the result
4213
+ * metadata so the client can route the response without scanning all sessions.
4099
4214
  */
4100
- async finishAuth(params) {
4101
- const { code } = params;
4102
- const oauthState = params.state;
4103
- const parsedState = parseOAuthState(oauthState);
4104
- const sessionId = parsedState?.sessionId || oauthState;
4105
- const session = await sessions.get(this.userId, sessionId);
4106
- if (!session) {
4107
- throw new Error("Session not found");
4108
- }
4109
- try {
4110
- const client = new MCPClient({
4111
- userId: this.userId,
4112
- sessionId,
4113
- // These fields are optional in MCPClient, but when rehydrating a known
4114
- // stored session on the server we pass them explicitly to preserve the
4115
- // original connection metadata instead of relying on lazy
4116
- // reloading during initialize().
4117
- serverId: session.serverId,
4118
- serverName: session.serverName,
4119
- serverUrl: session.serverUrl,
4120
- callbackUrl: session.callbackUrl,
4121
- // NOTE: transportType is intentionally omitted here.
4122
- // The session's stored transportType is a placeholder ('streamable-http')
4123
- // set before transport negotiation. Omitting it lets MCPClient auto-negotiate
4124
- // (try streamable_http → SSE fallback), which is critical for servers like
4125
- // Neon that only support SSE transport.
4126
- headers: session.headers
4127
- });
4128
- client.onConnectionEvent((event) => this.emitConnectionEvent(event));
4129
- await client.finishAuth(code, oauthState);
4130
- this.clients.set(sessionId, client);
4131
- const { result: tools } = await this.listPolicyFilteredTools(sessionId);
4132
- return { success: true, toolCount: tools.tools.length };
4133
- } catch (error) {
4134
- this.emitConnectionEvent({
4135
- type: "error",
4136
- sessionId,
4137
- serverId: session.serverId ?? "unknown",
4138
- error: error instanceof Error ? error.message : "OAuth completion failed",
4139
- errorType: "auth",
4140
- timestamp: Date.now()
4141
- });
4142
- throw error;
4143
- }
4215
+ async callTool(params) {
4216
+ const client = await this.getOrCreateClient(params.sessionId);
4217
+ const result = await createToolPolicyGateway(
4218
+ this.userId,
4219
+ params.sessionId,
4220
+ client
4221
+ ).callTool(params.toolName, params.toolArgs);
4222
+ return { ...result, _meta: { ...result._meta ?? {}, sessionId: params.sessionId } };
4144
4223
  }
4145
- /**
4146
- * List prompts from a session
4147
- */
4224
+ // -----------------------------------------------------------------------
4225
+ // Prompts
4226
+ // -----------------------------------------------------------------------
4227
+ /** Lists all prompts exposed by the remote MCP server. */
4148
4228
  async listPrompts(params) {
4149
- const { sessionId } = params;
4150
- const client = await this.getOrCreateClient(sessionId);
4229
+ const client = await this.getOrCreateClient(params.sessionId);
4151
4230
  const result = await client.listPrompts();
4152
4231
  return { prompts: result.prompts };
4153
4232
  }
4154
- /**
4155
- * Get a specific prompt
4156
- */
4233
+ /** Retrieves a specific prompt by name with optional arguments. */
4157
4234
  async getPrompt(params) {
4158
- const { sessionId, name, args } = params;
4159
- const client = await this.getOrCreateClient(sessionId);
4160
- return await client.getPrompt(name, args);
4235
+ const client = await this.getOrCreateClient(params.sessionId);
4236
+ return client.getPrompt(params.name, params.args);
4161
4237
  }
4162
- /**
4163
- * List resources from a session
4164
- */
4238
+ // -----------------------------------------------------------------------
4239
+ // Resources
4240
+ // -----------------------------------------------------------------------
4241
+ /** Lists all resources exposed by the remote MCP server. */
4165
4242
  async listResources(params) {
4166
- const { sessionId } = params;
4167
- const client = await this.getOrCreateClient(sessionId);
4243
+ const client = await this.getOrCreateClient(params.sessionId);
4168
4244
  const result = await client.listResources();
4169
4245
  return { resources: result.resources };
4170
4246
  }
4247
+ /** Reads a specific resource identified by URI. */
4248
+ async readResource(params) {
4249
+ const client = await this.getOrCreateClient(params.sessionId);
4250
+ return client.readResource(params.uri);
4251
+ }
4252
+ // -----------------------------------------------------------------------
4253
+ // Internal Helpers
4254
+ // -----------------------------------------------------------------------
4255
+ /** Resolves client metadata: `getClientMetadata()` → `clientDefaults` → `{}`. */
4256
+ async getResolvedClientMetadata(request) {
4257
+ let metadata = this.options.clientDefaults ? { ...this.options.clientDefaults } : {};
4258
+ if (this.options.getClientMetadata) {
4259
+ metadata = { ...metadata, ...await this.options.getClientMetadata(request) };
4260
+ }
4261
+ return metadata;
4262
+ }
4263
+ /** Ensures the given session exists in storage and throws otherwise. */
4264
+ async requireSession(sessionId) {
4265
+ const session = await sessions.get(this.userId, sessionId, { includeCredentials: true });
4266
+ if (!session) throw new Error(`Session ${sessionId} not found`);
4267
+ return session;
4268
+ }
4269
+ /** Finds an existing session matching the given serverId or serverUrl. */
4270
+ async findExistingSession(serverId, serverUrl) {
4271
+ const all = await sessions.list(this.userId);
4272
+ return all.find((s) => s.serverId === serverId || s.serverUrl === serverUrl);
4273
+ }
4274
+ /** Normalizes a serverId to max 12 chars (DeepSeek/OpenAI 64-char tool-name limit). */
4275
+ normalizeServerId(raw) {
4276
+ return raw && raw.length <= 12 ? raw : generateServerId();
4277
+ }
4171
4278
  /**
4172
- * Read a specific resource
4279
+ * Returns the cached in-memory transport for `sessionId`, or creates one
4280
+ * from the persisted session row (with credentials) and connects it.
4173
4281
  */
4174
- async readResource(params) {
4175
- const { sessionId, uri } = params;
4176
- const client = await this.getOrCreateClient(sessionId);
4177
- return client.readResource(uri);
4282
+ async getOrCreateClient(sessionId) {
4283
+ const existing = this.clients.get(sessionId);
4284
+ if (existing) return existing;
4285
+ const session = await this.requireSession(sessionId);
4286
+ if (session.enabled === false) {
4287
+ throw new Error("Session is disabled \u2014 re-enable it via updateSession to access tools");
4288
+ }
4289
+ const client = this.restoreClient(session);
4290
+ this.attachClientEvents(client);
4291
+ await client.connect();
4292
+ this.clients.set(sessionId, client);
4293
+ return client;
4178
4294
  }
4179
4295
  /**
4180
- * Emit connection event
4296
+ * Builds an `MCPClient` from a stored session row.
4297
+ *
4298
+ * Extracts `clientId` and `clientSecret` from the session's credential
4299
+ * fields and passes `hasSession: true` + `cachedCredentials` so the client
4300
+ * can skip redundant existence checks and credential reads.
4181
4301
  */
4182
- emitConnectionEvent(event) {
4183
- this.sendEvent(event);
4302
+ restoreClient(session) {
4303
+ const clientId = session.clientId ?? void 0;
4304
+ const clientSecret = session.clientInformation?.client_secret ?? void 0;
4305
+ return new MCPClient({
4306
+ userId: this.userId,
4307
+ sessionId: session.sessionId,
4308
+ serverId: session.serverId,
4309
+ serverName: session.serverName,
4310
+ serverUrl: session.serverUrl,
4311
+ callbackUrl: session.callbackUrl,
4312
+ transportType: session.transportType,
4313
+ headers: session.headers,
4314
+ clientId,
4315
+ clientSecret,
4316
+ hasSession: true,
4317
+ cachedCredentials: { tokens: session.tokens ?? void 0 },
4318
+ sessionStore: this.observedStore
4319
+ });
4184
4320
  }
4185
4321
  /**
4186
- * Cleanup and close all connections
4322
+ * Wires connection and observability events from the client into the
4323
+ * unified observability channel. Connection events go to `sendEvent`
4324
+ * (SSE stream); observability events go to `emitObs` (user callback
4325
+ * + SSE stream).
4187
4326
  */
4188
- async dispose() {
4189
- this.isActive = false;
4190
- if (this.heartbeatTimer) {
4191
- clearInterval(this.heartbeatTimer);
4327
+ attachClientEvents(client) {
4328
+ client.onConnectionEvent((e) => this.sendEvent(e));
4329
+ client.onObservabilityEvent((e) => this.sendEvent(e));
4330
+ }
4331
+ /**
4332
+ * Registers the client in the in-memory cache and attaches its event
4333
+ * listeners to the unified observability channel.
4334
+ */
4335
+ cacheClient(sessionId, client) {
4336
+ this.attachClientEvents(client);
4337
+ this.clients.set(sessionId, client);
4338
+ }
4339
+ /**
4340
+ * Attempts a `client.connect()` and, on success, discovers tools.
4341
+ *
4342
+ * If the server responds with `UnauthorizedError`, the session is
4343
+ * treated as pending OAuth — a successful result is returned with
4344
+ * the sessionId so the browser client can redirect to the auth URL.
4345
+ *
4346
+ * For any other error an `error` connection event is emitted and the
4347
+ * client is removed from the in-memory cache before re-throwing.
4348
+ */
4349
+ async connectAndDiscover(client, sessionId, serverId) {
4350
+ try {
4351
+ await client.connect();
4352
+ await this.listPolicyFilteredTools(sessionId);
4353
+ return { sessionId, success: true };
4354
+ } catch (error) {
4355
+ if (error instanceof UnauthorizedError) {
4356
+ this.clients.delete(sessionId);
4357
+ return { sessionId, success: true };
4358
+ }
4359
+ this.sendEvent(connectionErrorEvent(sessionId, serverId, error, "connection"));
4360
+ this.clients.delete(sessionId);
4361
+ throw error;
4192
4362
  }
4193
- await Promise.all(
4194
- Array.from(this.clients.values()).map((client) => client.disconnect())
4195
- );
4196
- this.clients.clear();
4363
+ }
4364
+ /** Starts the periodic heartbeat timer. */
4365
+ startHeartbeat() {
4366
+ const ms = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS;
4367
+ this.heartbeatTimer = setInterval(() => {
4368
+ if (this.active) {
4369
+ this.sendEvent({ level: "debug", message: "heartbeat", timestamp: Date.now() });
4370
+ }
4371
+ }, ms);
4197
4372
  }
4198
4373
  };
4199
4374
  function createSSEHandler(options) {
@@ -4222,8 +4397,7 @@ function createSSEHandler(options) {
4222
4397
  });
4223
4398
  req.on("end", async () => {
4224
4399
  try {
4225
- const request = JSON.parse(body);
4226
- await manager.handleRequest(request);
4400
+ await manager.handleRequest(JSON.parse(body));
4227
4401
  } catch {
4228
4402
  }
4229
4403
  });
@@ -4436,6 +4610,9 @@ var SSEClient = class {
4436
4610
  async getToolPolicy(sessionId) {
4437
4611
  return this.sendRequest("getToolPolicy", { sessionId });
4438
4612
  }
4613
+ async updateSession(sessionId, enabled) {
4614
+ return this.sendRequest("updateSession", { sessionId, enabled });
4615
+ }
4439
4616
  async listTools(sessionId) {
4440
4617
  return this.sendRequest("listTools", { sessionId });
4441
4618
  }
@@ -6367,6 +6544,8 @@ exports.createNextMcpHandler = createNextMcpHandler;
6367
6544
  exports.createRegexSearchToolDefinition = createRegexSearchToolDefinition;
6368
6545
  exports.createSSEHandler = createSSEHandler;
6369
6546
  exports.createSearchToolDefinition = createSearchToolDefinition;
6547
+ exports.decryptObject = decryptObject;
6548
+ exports.encryptObject = encryptObject;
6370
6549
  exports.executeMetaTool = executeMetaTool;
6371
6550
  exports.findToolByName = findToolByName;
6372
6551
  exports.getToolUiResourceUri = getToolUiResourceUri;
@@ -6380,5 +6559,6 @@ exports.onSessionMutation = onSessionMutation;
6380
6559
  exports.resolveMetaToolProxy = resolveMetaToolProxy;
6381
6560
  exports.sanitizeServerLabel = sanitizeServerLabel;
6382
6561
  exports.sessions = sessions;
6562
+ exports.withDbObservability = withDbObservability;
6383
6563
  //# sourceMappingURL=index.js.map
6384
6564
  //# sourceMappingURL=index.js.map