@mcp-ts/sdk 2.5.2 → 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-B3GPsPsi.d.mts → index-BrDkbut5.d.ts} +2 -1
  31. package/dist/{index-gNdSQTSz.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 -863
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +1048 -858
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{multi-session-client-CB4oDrQX.d.ts → multi-session-client-CQzymvHl.d.ts} +183 -227
  39. package/dist/{multi-session-client-xj3iQIv6.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 +1054 -864
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +1046 -859
  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-CsKVXbQB.d.mts → tool-router-C1n7OXbl.d.mts} +1 -1
  51. package/dist/{tool-router-C-Mw1_BQ.d.ts → tool-router-CBIawFnt.d.ts} +1 -1
  52. package/dist/{types-6SmXege4.d.mts → types-DX71u9gR.d.mts} +11 -3
  53. package/dist/{types-6SmXege4.d.ts → types-DX71u9gR.d.ts} +11 -3
  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 -629
  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 +149 -29
  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 -1
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,26 +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;
2107
+ if (this._clientId) {
2108
+ return {
2109
+ client_id: this._clientId,
2110
+ ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2111
+ };
2018
2112
  }
2019
- if (data.clientInformation) {
2020
- return data.clientInformation;
2113
+ const data = await this.getCredentials();
2114
+ if (this._cachedTokens === void 0) {
2115
+ this._cachedTokens = data.tokens ?? null;
2021
2116
  }
2022
- if (!this._clientId) {
2023
- 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
+ };
2024
2126
  }
2025
- return {
2026
- client_id: this._clientId,
2027
- ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2028
- };
2127
+ return void 0;
2029
2128
  }
2030
2129
  /**
2031
2130
  * Stores OAuth client information
@@ -2042,11 +2141,30 @@ var StorageOAuthClientProvider = class {
2042
2141
  */
2043
2142
  async saveTokens(tokens) {
2044
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
+ }
2045
2161
  }
2046
2162
  get authUrl() {
2047
2163
  return this._authUrl;
2048
2164
  }
2049
2165
  async state() {
2166
+ this._cachedCodeVerifier = void 0;
2167
+ this._hasCodeVerifier = false;
2050
2168
  const nonce = nanoid.nanoid(32);
2051
2169
  await this.patchCredentials({
2052
2170
  oauthState: {
@@ -2067,7 +2185,7 @@ var StorageOAuthClientProvider = class {
2067
2185
  if (parsed.sessionId !== this.sessionId) {
2068
2186
  return { valid: false, error: "OAuth state mismatch" };
2069
2187
  }
2070
- const data = await sessions.getCredentials(this.userId, parsed.sessionId);
2188
+ const data = await this._store.getCredentials(this.userId, parsed.sessionId);
2071
2189
  if (!data) {
2072
2190
  return { valid: false, error: "Session not found" };
2073
2191
  }
@@ -2092,49 +2210,77 @@ var StorageOAuthClientProvider = class {
2092
2210
  }
2093
2211
  async redirectToAuthorization(authUrl) {
2094
2212
  this._authUrl = authUrl.toString();
2095
- 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
+ }
2096
2226
  if (this.onRedirectCallback) {
2097
2227
  this.onRedirectCallback(authUrl.toString());
2098
2228
  }
2099
2229
  }
2100
2230
  async invalidateCredentials(scope) {
2101
2231
  if (scope === "all") {
2102
- await sessions.delete(this.userId, this.sessionId);
2232
+ this._cachedTokens = void 0;
2233
+ await this._store.delete(this.userId, this.sessionId);
2103
2234
  } else {
2104
2235
  const updates = {};
2105
2236
  if (scope === "client") {
2106
2237
  updates.clientInformation = null;
2107
2238
  updates.clientId = null;
2108
2239
  } else if (scope === "tokens") {
2240
+ this._cachedTokens = void 0;
2109
2241
  updates.tokens = null;
2110
2242
  } else if (scope === "verifier") {
2243
+ this._cachedCodeVerifier = void 0;
2244
+ this._hasCodeVerifier = false;
2111
2245
  updates.codeVerifier = null;
2112
2246
  }
2113
2247
  await this.patchCredentials(updates);
2114
2248
  }
2115
2249
  }
2116
2250
  async saveCodeVerifier(verifier) {
2117
- const data = await this.getCredentials();
2118
- if (data.codeVerifier) {
2251
+ if (this._hasCodeVerifier) {
2119
2252
  return;
2120
2253
  }
2121
- await this.patchCredentials({ codeVerifier: verifier });
2254
+ this._cachedCodeVerifier = verifier;
2255
+ this._hasCodeVerifier = true;
2122
2256
  }
2123
2257
  async codeVerifier() {
2124
- const data = await this.getCredentials();
2125
- if (data.clientId && !this._clientId) {
2126
- this._clientId = data.clientId;
2258
+ if (this._cachedCodeVerifier) {
2259
+ return this._cachedCodeVerifier;
2260
+ }
2261
+ const ctx = codeVerifierContext.getStore();
2262
+ if (ctx?.verifier) {
2263
+ return ctx.verifier;
2127
2264
  }
2128
- if (!data.codeVerifier) {
2129
- throw new Error("No code verifier found");
2265
+ const data = await this.getCredentials();
2266
+ if (data.codeVerifier) {
2267
+ return data.codeVerifier;
2130
2268
  }
2131
- return data.codeVerifier;
2269
+ throw new Error("No code verifier found");
2132
2270
  }
2133
2271
  async deleteCodeVerifier() {
2134
- await this.patchCredentials({ codeVerifier: null });
2272
+ this._cachedCodeVerifier = void 0;
2273
+ this._hasCodeVerifier = false;
2274
+ await this.patchCredentials({
2275
+ codeVerifier: null
2276
+ });
2135
2277
  }
2136
2278
  async tokens() {
2279
+ if (this._cachedTokens !== void 0) {
2280
+ return this._cachedTokens ?? void 0;
2281
+ }
2137
2282
  const data = await this.getCredentials();
2283
+ this._cachedTokens = data.tokens ?? null;
2138
2284
  if (data.clientId && !this._clientId) {
2139
2285
  this._clientId = data.clientId;
2140
2286
  }
@@ -2297,23 +2443,10 @@ var MCPClient = class {
2297
2443
  __publicField(this, "client");
2298
2444
  __publicField(this, "oauthProvider", null);
2299
2445
  __publicField(this, "transport", null);
2300
- __publicField(this, "userId");
2301
- __publicField(this, "serverId");
2302
- __publicField(this, "sessionId");
2303
- __publicField(this, "serverName");
2304
- __publicField(this, "transportType");
2305
- __publicField(this, "serverUrl");
2306
- __publicField(this, "callbackUrl");
2307
- __publicField(this, "onRedirect");
2308
- __publicField(this, "clientId");
2309
- __publicField(this, "clientSecret");
2310
- __publicField(this, "headers");
2311
- /** OAuth Client Metadata */
2312
- __publicField(this, "clientName");
2313
- __publicField(this, "clientUri");
2314
- __publicField(this, "logoUri");
2315
- __publicField(this, "policyUri");
2446
+ __publicField(this, "config");
2316
2447
  __publicField(this, "createdAt");
2448
+ __publicField(this, "_serverInfo");
2449
+ __publicField(this, "_store");
2317
2450
  /** Event emitters for connection lifecycle */
2318
2451
  __publicField(this, "_onConnectionEvent", new Emitter());
2319
2452
  __publicField(this, "onConnectionEvent", this._onConnectionEvent.event);
@@ -2329,21 +2462,8 @@ var MCPClient = class {
2329
2462
  * the memory when the client is no longer needed.
2330
2463
  */
2331
2464
  __publicField(this, "cachedTools", null);
2332
- this.serverUrl = options.serverUrl;
2333
- this.serverName = options.serverName;
2334
- this.callbackUrl = options.callbackUrl;
2335
- this.onRedirect = options.onRedirect;
2336
- this.userId = options.userId;
2337
- this.serverId = options.serverId;
2338
- this.sessionId = options.sessionId;
2339
- this.transportType = options.transportType;
2340
- this.clientId = options.clientId;
2341
- this.clientSecret = options.clientSecret;
2342
- this.headers = options.headers;
2343
- this.clientName = options.clientName;
2344
- this.clientUri = options.clientUri;
2345
- this.logoUri = options.logoUri;
2346
- this.policyUri = options.policyUri;
2465
+ this.config = { ...options };
2466
+ this._store = options.sessionStore ?? sessions;
2347
2467
  this.client = new index_js.Client(
2348
2468
  {
2349
2469
  name: MCP_CLIENT_NAME,
@@ -2360,6 +2480,21 @@ var MCPClient = class {
2360
2480
  }
2361
2481
  );
2362
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
+ }
2363
2498
  /**
2364
2499
  * Emit a connection state change event
2365
2500
  * @private
@@ -2367,13 +2502,13 @@ var MCPClient = class {
2367
2502
  emitStateChange(newState) {
2368
2503
  const previousState = this.currentState;
2369
2504
  this.currentState = newState;
2370
- if (!this.serverId) return;
2505
+ if (!this.config.serverId) return;
2371
2506
  this._onConnectionEvent.fire({
2372
2507
  type: "state_changed",
2373
- sessionId: this.sessionId,
2374
- serverId: this.serverId,
2375
- serverName: this.serverName || this.serverId,
2376
- 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 || "",
2377
2512
  createdAt: this.createdAt,
2378
2513
  state: newState,
2379
2514
  previousState,
@@ -2384,8 +2519,8 @@ var MCPClient = class {
2384
2519
  level: "info",
2385
2520
  message: `Connection state: ${previousState} \u2192 ${newState}`,
2386
2521
  displayMessage: `State changed to ${newState}`,
2387
- sessionId: this.sessionId,
2388
- serverId: this.serverId,
2522
+ sessionId: this.config.sessionId,
2523
+ serverId: this.config.serverId,
2389
2524
  payload: { previousState, newState },
2390
2525
  timestamp: Date.now(),
2391
2526
  id: nanoid.nanoid()
@@ -2396,11 +2531,11 @@ var MCPClient = class {
2396
2531
  * @private
2397
2532
  */
2398
2533
  emitError(error, errorType = "unknown") {
2399
- if (!this.serverId) return;
2534
+ if (!this.config.serverId) return;
2400
2535
  this._onConnectionEvent.fire({
2401
2536
  type: "error",
2402
- sessionId: this.sessionId,
2403
- serverId: this.serverId,
2537
+ sessionId: this.config.sessionId,
2538
+ serverId: this.config.serverId,
2404
2539
  error,
2405
2540
  errorType,
2406
2541
  timestamp: Date.now()
@@ -2410,8 +2545,8 @@ var MCPClient = class {
2410
2545
  level: "error",
2411
2546
  message: error,
2412
2547
  displayMessage: error,
2413
- sessionId: this.sessionId,
2414
- serverId: this.serverId,
2548
+ sessionId: this.config.sessionId,
2549
+ serverId: this.config.serverId,
2415
2550
  payload: { errorType, error },
2416
2551
  timestamp: Date.now(),
2417
2552
  id: nanoid.nanoid()
@@ -2422,11 +2557,11 @@ var MCPClient = class {
2422
2557
  * @private
2423
2558
  */
2424
2559
  emitProgress(message) {
2425
- if (!this.serverId) return;
2560
+ if (!this.config.serverId) return;
2426
2561
  this._onConnectionEvent.fire({
2427
2562
  type: "progress",
2428
- sessionId: this.sessionId,
2429
- serverId: this.serverId,
2563
+ sessionId: this.config.sessionId,
2564
+ serverId: this.config.serverId,
2430
2565
  message,
2431
2566
  timestamp: Date.now()
2432
2567
  });
@@ -2444,14 +2579,14 @@ var MCPClient = class {
2444
2579
  * @private
2445
2580
  */
2446
2581
  getTransport(type) {
2447
- if (!this.serverUrl) {
2582
+ if (!this.config.serverUrl) {
2448
2583
  throw new Error("Server URL is required to create transport");
2449
2584
  }
2450
- const baseUrl = new URL(this.serverUrl);
2451
- 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");
2452
2587
  const transportOptions = {
2453
2588
  ...!hasAuthorizationHeader && { authProvider: this.oauthProvider },
2454
- ...this.headers && { requestInit: { headers: this.headers } },
2589
+ ...this.config.headers && { requestInit: { headers: this.config.headers } },
2455
2590
  /**
2456
2591
  * Custom fetch implementation to handle connection timeouts.
2457
2592
  * Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
@@ -2489,78 +2624,74 @@ var MCPClient = class {
2489
2624
  * @private
2490
2625
  */
2491
2626
  async ensureSession() {
2492
- if (this.oauthProvider) {
2493
- return;
2494
- }
2627
+ if (this.oauthProvider) return;
2495
2628
  this.emitStateChange("INITIALIZING");
2496
2629
  this.emitProgress("Loading session configuration...");
2497
- let existingSession = null;
2498
- if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
2499
- existingSession = await sessions.get(this.userId, this.sessionId);
2500
- if (!existingSession) {
2501
- 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}`);
2502
2634
  }
2503
- this.serverUrl = this.serverUrl || existingSession.serverUrl;
2504
- this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
2505
- this.serverName = this.serverName || existingSession.serverName;
2506
- this.serverId = this.serverId || existingSession.serverId || "unknown";
2507
- this.headers = this.headers || existingSession.headers;
2508
- this.createdAt = existingSession.createdAt;
2509
- }
2510
- 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) {
2511
2643
  throw new Error("Missing required connection metadata");
2512
2644
  }
2513
- if (!this.oauthProvider) {
2514
- if (!this.serverId) {
2515
- throw new Error("serverId required for OAuth provider initialization");
2516
- }
2517
- this.oauthProvider = new StorageOAuthClientProvider({
2518
- userId: this.userId,
2519
- serverId: this.serverId,
2520
- sessionId: this.sessionId,
2521
- redirectUrl: this.callbackUrl,
2522
- clientName: this.clientName,
2523
- clientUri: this.clientUri,
2524
- logoUri: this.logoUri,
2525
- policyUri: this.policyUri,
2526
- clientId: this.clientId,
2527
- clientSecret: this.clientSecret,
2528
- onRedirect: (redirectUrl) => {
2529
- if (this.onRedirect) {
2530
- this.onRedirect(redirectUrl);
2531
- }
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
+ });
2532
2667
  }
2533
- });
2534
- }
2535
- if (existingSession === null) {
2536
- existingSession = await sessions.get(this.userId, this.sessionId);
2537
- }
2538
- 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) {
2539
2675
  this.createdAt = Date.now();
2540
2676
  const updatedAt = this.createdAt;
2541
2677
  this._onObservabilityEvent.fire({
2542
2678
  type: "mcp:client:session_created",
2543
2679
  level: "info",
2544
- message: `Creating pending session ${this.sessionId} for connection setup`,
2545
- sessionId: this.sessionId,
2546
- serverId: this.serverId,
2680
+ message: `Creating pending session ${this.config.sessionId} for connection setup`,
2681
+ sessionId: this.config.sessionId,
2682
+ serverId: this.config.serverId,
2547
2683
  timestamp: Date.now(),
2548
2684
  id: nanoid.nanoid()
2549
2685
  });
2550
- await sessions.create({
2551
- sessionId: this.sessionId,
2552
- userId: this.userId,
2553
- serverId: this.serverId,
2554
- serverName: this.serverName,
2555
- serverUrl: this.serverUrl,
2556
- callbackUrl: this.callbackUrl,
2557
- transportType: this.transportType || "streamable-http",
2558
- headers: this.headers,
2559
- createdAt: this.createdAt,
2686
+ await this._store.create({
2687
+ ...this.session,
2560
2688
  updatedAt,
2561
2689
  status: "pending"
2562
2690
  });
2563
2691
  }
2692
+ if (!existingSession && this.oauthProvider instanceof StorageOAuthClientProvider) {
2693
+ await this.oauthProvider.initializeCredentials();
2694
+ }
2564
2695
  }
2565
2696
  /**
2566
2697
  * Saves current session state to the session store
@@ -2568,34 +2699,14 @@ var MCPClient = class {
2568
2699
  * @param status - Session lifecycle status used by storage cleanup
2569
2700
  * @private
2570
2701
  */
2571
- async saveSession(status = "active", existingSession) {
2572
- 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) {
2573
2704
  return;
2574
2705
  }
2575
- const sessionData = {
2576
- sessionId: this.sessionId,
2577
- userId: this.userId,
2578
- serverId: this.serverId,
2579
- serverName: this.serverName,
2580
- serverUrl: this.serverUrl,
2581
- callbackUrl: this.callbackUrl,
2582
- transportType: this.transportType || "streamable-http",
2583
- headers: this.headers,
2584
- createdAt: this.createdAt || Date.now(),
2585
- updatedAt: Date.now(),
2706
+ await this._store.update(this.config.userId, this.config.sessionId, {
2707
+ ...this.session,
2586
2708
  status
2587
- };
2588
- if (status === "active") {
2589
- sessionData.authUrl = null;
2590
- }
2591
- if (existingSession === void 0) {
2592
- existingSession = await sessions.get(this.userId, this.sessionId);
2593
- }
2594
- if (existingSession) {
2595
- await sessions.update(this.userId, this.sessionId, sessionData);
2596
- } else {
2597
- await sessions.create(sessionData);
2598
- }
2709
+ });
2599
2710
  }
2600
2711
  /**
2601
2712
  * Removes transient setup/auth sessions without masking the original error.
@@ -2603,7 +2714,7 @@ var MCPClient = class {
2603
2714
  */
2604
2715
  async deleteTransientSession() {
2605
2716
  try {
2606
- await sessions.delete(this.userId, this.sessionId);
2717
+ await this._store.delete(this.config.userId, this.config.sessionId);
2607
2718
  } catch {
2608
2719
  }
2609
2720
  }
@@ -2613,7 +2724,7 @@ var MCPClient = class {
2613
2724
  * @private
2614
2725
  */
2615
2726
  async tryConnect() {
2616
- const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
2727
+ const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
2617
2728
  let lastError;
2618
2729
  for (const currentType of transportsToTry) {
2619
2730
  const isLastAttempt = currentType === transportsToTry[transportsToTry.length - 1];
@@ -2621,6 +2732,7 @@ var MCPClient = class {
2621
2732
  const transport = this.getTransport(currentType);
2622
2733
  this.transport = transport;
2623
2734
  await this.client.connect(transport);
2735
+ this._serverInfo = this.client.getServerVersion();
2624
2736
  return { transportType: currentType };
2625
2737
  } catch (error) {
2626
2738
  lastError = error;
@@ -2636,8 +2748,8 @@ var MCPClient = class {
2636
2748
  this._onObservabilityEvent.fire({
2637
2749
  level: "warn",
2638
2750
  message: `Transport ${currentType} failed, falling back`,
2639
- sessionId: this.sessionId,
2640
- serverId: this.serverId,
2751
+ sessionId: this.config.sessionId,
2752
+ serverId: this.config.serverId,
2641
2753
  metadata: {
2642
2754
  failedTransport: currentType,
2643
2755
  error: errorMessage
@@ -2680,15 +2792,15 @@ var MCPClient = class {
2680
2792
  try {
2681
2793
  this.emitStateChange("CONNECTING");
2682
2794
  const { transportType } = await this.tryConnect();
2683
- this.transportType = transportType;
2795
+ this.config.transportType = transportType;
2684
2796
  this.emitStateChange("CONNECTED");
2685
2797
  this.emitProgress("Connected successfully");
2686
2798
  this._onObservabilityEvent.fire({
2687
2799
  type: "mcp:client:session_saved",
2688
2800
  level: "info",
2689
- message: `Saving active session ${this.sessionId} (connect success)`,
2690
- sessionId: this.sessionId,
2691
- serverId: this.serverId,
2801
+ message: `Saving active session ${this.config.sessionId} (connect success)`,
2802
+ sessionId: this.config.sessionId,
2803
+ serverId: this.config.serverId,
2692
2804
  timestamp: Date.now(),
2693
2805
  id: nanoid.nanoid()
2694
2806
  });
@@ -2711,23 +2823,23 @@ var MCPClient = class {
2711
2823
  this._onObservabilityEvent.fire({
2712
2824
  type: "mcp:client:session_saved",
2713
2825
  level: "info",
2714
- message: `Saving pending OAuth session ${this.sessionId}`,
2715
- sessionId: this.sessionId,
2716
- serverId: this.serverId,
2826
+ message: `Saving pending OAuth session ${this.config.sessionId}`,
2827
+ sessionId: this.config.sessionId,
2828
+ serverId: this.config.serverId,
2717
2829
  timestamp: Date.now(),
2718
2830
  id: nanoid.nanoid()
2719
2831
  });
2720
2832
  await this.saveSession("pending");
2721
- if (this.serverId) {
2833
+ if (this.config.serverId) {
2722
2834
  this._onConnectionEvent.fire({
2723
2835
  type: "auth_required",
2724
- sessionId: this.sessionId,
2725
- serverId: this.serverId,
2836
+ sessionId: this.config.sessionId,
2837
+ serverId: this.config.serverId,
2726
2838
  authUrl,
2727
2839
  timestamp: Date.now()
2728
2840
  });
2729
- if (authUrl && this.onRedirect) {
2730
- this.onRedirect(authUrl);
2841
+ if (authUrl && this.config.onRedirect) {
2842
+ this.config.onRedirect(authUrl);
2731
2843
  }
2732
2844
  }
2733
2845
  throw new UnauthorizedError("OAuth authorization required");
@@ -2736,9 +2848,9 @@ var MCPClient = class {
2736
2848
  this.emitError(errorMessage, "connection");
2737
2849
  this.emitStateChange("FAILED");
2738
2850
  try {
2739
- const existingSession = await sessions.get(this.userId, this.sessionId);
2851
+ const existingSession = await this._store.get(this.config.userId, this.config.sessionId);
2740
2852
  if (!existingSession || existingSession.status !== "active") {
2741
- await sessions.delete(this.userId, this.sessionId);
2853
+ await this._store.delete(this.config.userId, this.config.sessionId);
2742
2854
  }
2743
2855
  } catch {
2744
2856
  }
@@ -2763,16 +2875,16 @@ var MCPClient = class {
2763
2875
  throw new Error(error);
2764
2876
  }
2765
2877
  if (state) {
2766
- const stateCheck = await this.oauthProvider.checkState(state);
2767
- if (!stateCheck.valid) {
2768
- const error = stateCheck.error || "Invalid OAuth state";
2769
- 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");
2770
2883
  this.emitStateChange("FAILED");
2771
- throw new Error(error);
2884
+ throw error;
2772
2885
  }
2773
- await this.oauthProvider.consumeState(state);
2774
2886
  }
2775
- const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
2887
+ const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
2776
2888
  let lastError;
2777
2889
  let tokensExchanged = false;
2778
2890
  let authenticatedStateEmitted = false;
@@ -2799,14 +2911,15 @@ var MCPClient = class {
2799
2911
  }
2800
2912
  }
2801
2913
  await this.client.connect(this.transport);
2802
- this.transportType = currentType;
2914
+ this._serverInfo = this.client.getServerVersion();
2915
+ this.config.transportType = currentType;
2803
2916
  this.emitStateChange("CONNECTED");
2804
2917
  this._onObservabilityEvent.fire({
2805
2918
  type: "mcp:client:session_saved",
2806
2919
  level: "info",
2807
- message: `Saving active session ${this.sessionId} (OAuth complete)`,
2808
- sessionId: this.sessionId,
2809
- serverId: this.serverId,
2920
+ message: `Saving active session ${this.config.sessionId} (OAuth complete)`,
2921
+ sessionId: this.config.sessionId,
2922
+ serverId: this.config.serverId,
2810
2923
  timestamp: Date.now(),
2811
2924
  id: nanoid.nanoid()
2812
2925
  });
@@ -2879,11 +2992,11 @@ var MCPClient = class {
2879
2992
  this.emitStateChange("DISCOVERING");
2880
2993
  try {
2881
2994
  const result = await this.fetchTools();
2882
- if (this.serverId) {
2995
+ if (this.config.serverId) {
2883
2996
  this._onConnectionEvent.fire({
2884
2997
  type: "tools_discovered",
2885
- sessionId: this.sessionId,
2886
- serverId: this.serverId,
2998
+ sessionId: this.config.sessionId,
2999
+ serverId: this.config.serverId,
2887
3000
  toolCount: result.tools.length,
2888
3001
  tools: result.tools,
2889
3002
  timestamp: Date.now()
@@ -2923,8 +3036,8 @@ var MCPClient = class {
2923
3036
  level: "info",
2924
3037
  message: `Tool ${toolName} called successfully`,
2925
3038
  displayMessage: `Called tool ${toolName}`,
2926
- sessionId: this.sessionId,
2927
- serverId: this.serverId,
3039
+ sessionId: this.config.sessionId,
3040
+ serverId: this.config.serverId,
2928
3041
  payload: {
2929
3042
  toolName,
2930
3043
  args: toolArgs
@@ -2940,8 +3053,8 @@ var MCPClient = class {
2940
3053
  level: "error",
2941
3054
  message: errorMessage,
2942
3055
  displayMessage: `Failed to call tool ${toolName}`,
2943
- sessionId: this.sessionId,
2944
- serverId: this.serverId,
3056
+ sessionId: this.config.sessionId,
3057
+ serverId: this.config.serverId,
2945
3058
  payload: {
2946
3059
  errorType: "tool_execution",
2947
3060
  error: errorMessage,
@@ -3089,8 +3202,8 @@ var MCPClient = class {
3089
3202
  type: "mcp:client:error",
3090
3203
  level: "warn",
3091
3204
  message: "Initialization failed during clearSession",
3092
- sessionId: this.sessionId,
3093
- serverId: this.serverId,
3205
+ sessionId: this.config.sessionId,
3206
+ serverId: this.config.serverId,
3094
3207
  payload: { error: String(error) },
3095
3208
  timestamp: Date.now(),
3096
3209
  id: nanoid.nanoid()
@@ -3099,7 +3212,7 @@ var MCPClient = class {
3099
3212
  if (this.oauthProvider) {
3100
3213
  await this.oauthProvider.invalidateCredentials("all");
3101
3214
  }
3102
- await sessions.delete(this.userId, this.sessionId);
3215
+ await this._store.delete(this.config.userId, this.config.sessionId);
3103
3216
  await this.disconnect();
3104
3217
  }
3105
3218
  /**
@@ -3133,19 +3246,19 @@ var MCPClient = class {
3133
3246
  }
3134
3247
  this.oauthProvider = null;
3135
3248
  this.transport = null;
3136
- if (this.serverId) {
3249
+ if (this.config.serverId) {
3137
3250
  this._onConnectionEvent.fire({
3138
3251
  type: "disconnected",
3139
- sessionId: this.sessionId,
3140
- serverId: this.serverId,
3252
+ sessionId: this.config.sessionId,
3253
+ serverId: this.config.serverId,
3141
3254
  timestamp: Date.now()
3142
3255
  });
3143
3256
  this._onObservabilityEvent.fire({
3144
3257
  type: "mcp:client:disconnect",
3145
3258
  level: "info",
3146
- message: `Disconnected from ${this.serverId}`,
3147
- sessionId: this.sessionId,
3148
- serverId: this.serverId,
3259
+ message: `Disconnected from ${this.config.serverId}`,
3260
+ sessionId: this.config.sessionId,
3261
+ serverId: this.config.serverId,
3149
3262
  payload: {},
3150
3263
  timestamp: Date.now(),
3151
3264
  id: nanoid.nanoid()
@@ -3170,42 +3283,52 @@ var MCPClient = class {
3170
3283
  * @returns Server URL or empty string if not set
3171
3284
  */
3172
3285
  getServerUrl() {
3173
- return this.serverUrl || "";
3286
+ return this.config.serverUrl || "";
3174
3287
  }
3175
3288
  /**
3176
3289
  * Gets the OAuth callback URL
3177
3290
  * @returns Callback URL or empty string if not set
3178
3291
  */
3179
3292
  getCallbackUrl() {
3180
- return this.callbackUrl || "";
3293
+ return this.config.callbackUrl || "";
3181
3294
  }
3182
3295
  /**
3183
3296
  * Gets the transport type being used
3184
3297
  * @returns Transport type (defaults to 'streamable-http')
3185
3298
  */
3186
3299
  getTransportType() {
3187
- 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;
3188
3309
  }
3189
3310
  /**
3190
- * 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.
3191
3314
  * @returns Server name or undefined
3192
3315
  */
3193
3316
  getServerName() {
3194
- return this.serverName;
3317
+ return this.config.serverName;
3195
3318
  }
3196
3319
  /**
3197
3320
  * Gets the server ID
3198
3321
  * @returns Server ID or undefined
3199
3322
  */
3200
3323
  getServerId() {
3201
- return this.serverId;
3324
+ return this.config.serverId;
3202
3325
  }
3203
3326
  /**
3204
3327
  * Gets the session ID
3205
3328
  * @returns Session ID
3206
3329
  */
3207
3330
  getSessionId() {
3208
- return this.sessionId;
3331
+ return this.config.sessionId;
3209
3332
  }
3210
3333
  };
3211
3334
 
@@ -3232,6 +3355,14 @@ var ToolPolicyGateway = class {
3232
3355
  getServerId() {
3233
3356
  return this.client.getServerId?.();
3234
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
+ }
3235
3366
  /**
3236
3367
  * Returns the human-readable server name from the underlying client, if available.
3237
3368
  */
@@ -3360,6 +3491,7 @@ var MultiSessionClient = class {
3360
3491
  __publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
3361
3492
  __publicField(this, "userId");
3362
3493
  __publicField(this, "options");
3494
+ __publicField(this, "_store");
3363
3495
  this.userId = userId;
3364
3496
  this.options = {
3365
3497
  timeout: DEFAULT_TIMEOUT_MS,
@@ -3367,6 +3499,7 @@ var MultiSessionClient = class {
3367
3499
  retryDelay: DEFAULT_RETRY_DELAY_MS,
3368
3500
  ...options
3369
3501
  };
3502
+ this._store = options.onObservabilityEvent ? withDbObservability(options.sessionStore ?? sessions, options.onObservabilityEvent) : options.sessionStore ?? sessions;
3370
3503
  }
3371
3504
  // -----------------------------------------------------------------------
3372
3505
  // Public API
@@ -3450,9 +3583,9 @@ var MultiSessionClient = class {
3450
3583
  * to querying the storage backend via `sessions.list(userId)`.
3451
3584
  */
3452
3585
  async fetchActiveSessions() {
3453
- 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);
3454
3587
  return sessionList.filter(
3455
- (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
3588
+ (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active" && s.enabled !== false
3456
3589
  );
3457
3590
  }
3458
3591
  /**
@@ -3525,8 +3658,13 @@ var MultiSessionClient = class {
3525
3658
  callbackUrl: session.callbackUrl,
3526
3659
  serverName: session.serverName,
3527
3660
  transportType: session.transportType,
3528
- headers: session.headers
3661
+ headers: session.headers,
3662
+ hasSession: true,
3663
+ sessionStore: this._store
3529
3664
  });
3665
+ if (this.options.onObservabilityEvent) {
3666
+ client.onObservabilityEvent(this.options.onObservabilityEvent);
3667
+ }
3530
3668
  const timeoutMs = this.options.timeout;
3531
3669
  let timeoutTimer;
3532
3670
  const timeoutPromise = new Promise((_, reject) => {
@@ -3585,130 +3723,188 @@ function isConnectionEvent(event) {
3585
3723
  }
3586
3724
 
3587
3725
  // src/server/handlers/sse-handler.ts
3588
- var DEFAULT_HEARTBEAT_INTERVAL = 3e4;
3726
+ var DEFAULT_HEARTBEAT_MS = 3e4;
3589
3727
  function normalizeHeaders(headers) {
3590
3728
  if (!headers || typeof headers !== "object") return void 0;
3591
- 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);
3592
3730
  return entries.length > 0 ? Object.fromEntries(entries) : void 0;
3593
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
+ }
3594
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
+ */
3595
3762
  constructor(options, sendEvent) {
3596
3763
  this.options = options;
3597
3764
  this.sendEvent = sendEvent;
3765
+ // -----------------------------------------------------------------------
3766
+ // State
3767
+ // -----------------------------------------------------------------------
3598
3768
  __publicField(this, "userId");
3769
+ /** Active MCP transports keyed by sessionId. */
3599
3770
  __publicField(this, "clients", /* @__PURE__ */ new Map());
3771
+ /** Instrumented session store — always wraps `sessions` with DB observability. */
3772
+ __publicField(this, "observedStore");
3600
3773
  __publicField(this, "heartbeatTimer");
3601
- __publicField(this, "isActive", true);
3774
+ __publicField(this, "active", true);
3602
3775
  this.userId = options.userId;
3776
+ this.observedStore = withDbObservability(sessions, (event) => this.sendEvent(event));
3603
3777
  this.startHeartbeat();
3604
3778
  }
3779
+ // -----------------------------------------------------------------------
3780
+ // Public API
3781
+ // -----------------------------------------------------------------------
3605
3782
  /**
3606
- * Get resolved client metadata (dynamic > static > defaults)
3607
- */
3608
- async getResolvedClientMetadata(request) {
3609
- let metadata = {};
3610
- if (this.options.clientDefaults) {
3611
- metadata = { ...this.options.clientDefaults };
3612
- }
3613
- if (this.options.getClientMetadata) {
3614
- const dynamicMetadata = await this.options.getClientMetadata(request);
3615
- metadata = { ...metadata, ...dynamicMetadata };
3616
- }
3617
- return metadata;
3618
- }
3619
- /**
3620
- * Start heartbeat to keep connection alive
3621
- */
3622
- startHeartbeat() {
3623
- const interval = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL;
3624
- this.heartbeatTimer = setInterval(() => {
3625
- if (this.isActive) {
3626
- this.sendEvent({
3627
- level: "debug",
3628
- message: "heartbeat",
3629
- timestamp: Date.now()
3630
- });
3631
- }
3632
- }, interval);
3633
- }
3634
- /**
3635
- * Handle incoming RPC requests
3636
- * 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).
3637
3793
  */
3638
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
+ });
3639
3805
  try {
3640
- let result;
3641
- switch (request.method) {
3642
- case "listSessions":
3643
- result = await this.listSessions();
3644
- break;
3645
- case "connect":
3646
- result = await this.connect(request.params);
3647
- break;
3648
- case "reconnect":
3649
- result = await this.reconnect(request.params);
3650
- break;
3651
- case "disconnect":
3652
- result = await this.disconnect(request.params);
3653
- break;
3654
- case "listTools":
3655
- result = await this.listTools(request.params);
3656
- break;
3657
- case "setToolPolicy":
3658
- result = await this.setToolPolicy(request.params);
3659
- break;
3660
- case "getToolPolicy":
3661
- result = await this.getToolPolicy(request.params);
3662
- break;
3663
- case "callTool":
3664
- result = await this.callTool(request.params);
3665
- break;
3666
- case "getSession":
3667
- result = await this.getSession(request.params);
3668
- break;
3669
- case "finishAuth":
3670
- result = await this.finishAuth(request.params);
3671
- break;
3672
- case "listPrompts":
3673
- result = await this.listPrompts(request.params);
3674
- break;
3675
- case "getPrompt":
3676
- result = await this.getPrompt(request.params);
3677
- break;
3678
- case "listResources":
3679
- result = await this.listResources(request.params);
3680
- break;
3681
- case "readResource":
3682
- result = await this.readResource(request.params);
3683
- break;
3684
- default:
3685
- throw new Error(`Unknown method: ${request.method}`);
3686
- }
3687
- const response = {
3688
- id: request.id,
3689
- result
3690
- };
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 };
3691
3816
  this.sendEvent(response);
3692
3817
  return response;
3693
3818
  } catch (error) {
3694
- 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 = {
3695
3828
  id: request.id,
3696
3829
  error: {
3697
3830
  code: RpcErrorCodes.EXECUTION_ERROR,
3698
3831
  message: error instanceof Error ? error.message : "Unknown error"
3699
3832
  }
3700
3833
  };
3701
- this.sendEvent(errorResponse);
3702
- return errorResponse;
3834
+ this.sendEvent(response);
3835
+ return response;
3836
+ }
3837
+ }
3838
+ /**
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}`);
3703
3894
  }
3704
3895
  }
3896
+ // -----------------------------------------------------------------------
3897
+ // Session Query
3898
+ // -----------------------------------------------------------------------
3705
3899
  /**
3706
- * Get all sessions for the current userId
3900
+ * Lists all sessions owned by the current user.
3901
+ *
3902
+ * Returns a lightweight view — session metadata only, no credential fields.
3707
3903
  */
3708
3904
  async listSessions() {
3709
- const sessionList = await sessions.list(this.userId);
3905
+ const all = await sessions.list(this.userId);
3710
3906
  return {
3711
- sessions: sessionList.map((s) => ({
3907
+ sessions: all.map((s) => ({
3712
3908
  sessionId: s.sessionId,
3713
3909
  serverId: s.serverId,
3714
3910
  serverName: s.serverName,
@@ -3717,211 +3913,203 @@ var SSEConnectionManager = class {
3717
3913
  createdAt: s.createdAt,
3718
3914
  updatedAt: s.updatedAt ?? s.createdAt,
3719
3915
  status: s.status ?? "pending",
3720
- toolPolicy: s.toolPolicy
3916
+ toolPolicy: s.toolPolicy,
3917
+ enabled: s.enabled ?? true
3721
3918
  }))
3722
3919
  };
3723
3920
  }
3724
3921
  /**
3725
- * 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.
3726
3927
  */
3727
- async connect(params) {
3728
- const { serverName, serverUrl, callbackUrl, transportType } = params;
3729
- const headers = normalizeHeaders(params.headers);
3730
- const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : generateServerId();
3731
- const existingSessions = await sessions.list(this.userId);
3732
- const duplicate = existingSessions.find(
3733
- (s) => s.serverId === serverId || s.serverUrl === serverUrl
3734
- );
3735
- if (duplicate) {
3736
- if (duplicate.status === "pending") {
3737
- await this.getSession({ sessionId: duplicate.sessionId });
3738
- return {
3739
- sessionId: duplicate.sessionId,
3740
- success: true
3741
- };
3742
- }
3743
- throw new Error(`Connection already exists for server: ${duplicate.serverUrl || duplicate.serverId} (${duplicate.serverName})`);
3744
- }
3745
- const sessionId = await sessions.generateSessionId();
3928
+ async getSession(params) {
3929
+ const session = await this.requireSession(params.sessionId);
3930
+ this.sendEvent(validatingStateEvent(params.sessionId, session));
3746
3931
  try {
3747
- const clientMetadata = await this.getResolvedClientMetadata();
3748
- const client = new MCPClient({
3749
- userId: this.userId,
3750
- sessionId,
3751
- serverId,
3752
- serverName,
3753
- serverUrl,
3754
- callbackUrl,
3755
- transportType,
3756
- headers,
3757
- ...clientMetadata
3758
- // Spread client metadata (clientName, clientUri, logoUri, policyUri)
3759
- });
3760
- this.clients.set(sessionId, client);
3761
- client.onConnectionEvent((event) => {
3762
- this.emitConnectionEvent(event);
3763
- });
3764
- client.onObservabilityEvent((event) => {
3765
- this.sendEvent(event);
3766
- });
3932
+ const client = this.restoreClient(session);
3933
+ this.attachClientEvents(client);
3767
3934
  await client.connect();
3768
- await this.listPolicyFilteredTools(sessionId);
3769
- return {
3770
- sessionId,
3771
- success: true
3772
- };
3935
+ this.clients.set(params.sessionId, client);
3936
+ const { result } = await this.listPolicyFilteredTools(params.sessionId);
3937
+ return { success: true, toolCount: result.tools.length };
3773
3938
  } catch (error) {
3774
- if (error instanceof UnauthorizedError) {
3775
- this.clients.delete(sessionId);
3776
- return {
3777
- sessionId,
3778
- success: true
3779
- };
3780
- }
3781
- this.emitConnectionEvent({
3782
- type: "error",
3783
- sessionId,
3784
- serverId,
3785
- error: error instanceof Error ? error.message : "Connection failed",
3786
- errorType: "connection",
3787
- timestamp: Date.now()
3788
- });
3789
- this.clients.delete(sessionId);
3939
+ this.sendEvent(connectionErrorEvent(params.sessionId, session.serverId, error, "validation"));
3790
3940
  throw error;
3791
3941
  }
3792
3942
  }
3943
+ // -----------------------------------------------------------------------
3944
+ // Connection Lifecycle
3945
+ // -----------------------------------------------------------------------
3793
3946
  /**
3794
- * Reconnect to an MCP server — tears down the active client transport/connection
3795
- * 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).
3796
3955
  */
3797
- async reconnect(params) {
3798
- const { serverId: rawServerId, serverName, serverUrl, callbackUrl, transportType } = params;
3956
+ async connect(params) {
3799
3957
  const headers = normalizeHeaders(params.headers);
3800
- const serverId = rawServerId && rawServerId.length <= 12 ? rawServerId : generateServerId();
3801
- const existingSessions = await sessions.list(this.userId);
3802
- const duplicate = existingSessions.find(
3803
- (s) => s.serverId === serverId || s.serverUrl === serverUrl
3804
- );
3805
- const sessionId = duplicate ? duplicate.sessionId : await sessions.generateSessionId();
3806
- if (duplicate) {
3807
- const existingClient = this.clients.get(duplicate.sessionId);
3808
- if (existingClient) {
3809
- await existingClient.disconnect();
3810
- 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
+ }));
3811
3966
  }
3967
+ throw new Error(
3968
+ `Connection already exists for server: ${existing.serverUrl ?? existing.serverId} (${existing.serverName})`
3969
+ );
3812
3970
  }
3813
- try {
3814
- const clientMetadata = await this.getResolvedClientMetadata();
3815
- const client = new MCPClient({
3816
- userId: this.userId,
3817
- sessionId,
3818
- serverId,
3819
- serverName,
3820
- serverUrl,
3821
- callbackUrl,
3822
- transportType,
3823
- headers,
3824
- ...clientMetadata
3825
- });
3826
- this.clients.set(sessionId, client);
3827
- client.onConnectionEvent((event) => {
3828
- this.emitConnectionEvent(event);
3829
- });
3830
- client.onObservabilityEvent((event) => {
3831
- this.sendEvent(event);
3832
- });
3833
- await client.connect();
3834
- await this.listPolicyFilteredTools(sessionId);
3835
- return { sessionId, success: true };
3836
- } catch (error) {
3837
- if (error instanceof UnauthorizedError) {
3838
- this.clients.delete(sessionId);
3839
- 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);
3840
4003
  }
3841
- this.emitConnectionEvent({
3842
- type: "error",
3843
- sessionId,
3844
- serverId,
3845
- error: error instanceof Error ? error.message : "Connection failed",
3846
- errorType: "connection",
3847
- timestamp: Date.now()
3848
- });
3849
- this.clients.delete(sessionId);
3850
- throw error;
3851
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);
3852
4020
  }
3853
4021
  /**
3854
- * 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.
3855
4028
  */
3856
4029
  async disconnect(params) {
3857
- const { sessionId } = params;
3858
- const client = this.clients.get(sessionId);
4030
+ const client = this.clients.get(params.sessionId);
3859
4031
  if (client) {
3860
4032
  await client.clearSession();
3861
- this.clients.delete(sessionId);
4033
+ this.clients.delete(params.sessionId);
3862
4034
  } else {
3863
- await sessions.delete(this.userId, sessionId);
4035
+ await sessions.delete(this.userId, params.sessionId);
3864
4036
  }
3865
4037
  return { success: true };
3866
4038
  }
4039
+ // -----------------------------------------------------------------------
4040
+ // OAuth
4041
+ // -----------------------------------------------------------------------
3867
4042
  /**
3868
- * 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.
3869
4053
  */
3870
- async getOrCreateClient(sessionId) {
3871
- const existing = this.clients.get(sessionId);
3872
- if (existing) {
3873
- return existing;
3874
- }
3875
- const session = await sessions.get(this.userId, sessionId);
3876
- if (!session) {
3877
- 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;
3878
4087
  }
3879
- const client = new MCPClient({
3880
- userId: this.userId,
3881
- sessionId,
3882
- // These fields are optional in MCPClient, but when rehydrating a known
3883
- // stored session on the server we pass them explicitly to preserve the
3884
- // original transport/connection metadata instead of relying on lazy
3885
- // reloading during initialize().
3886
- serverId: session.serverId,
3887
- serverName: session.serverName,
3888
- serverUrl: session.serverUrl,
3889
- callbackUrl: session.callbackUrl,
3890
- transportType: session.transportType,
3891
- headers: session.headers
3892
- });
3893
- client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3894
- client.onObservabilityEvent((event) => this.sendEvent(event));
3895
- await client.connect();
3896
- this.clients.set(sessionId, client);
3897
- return client;
3898
4088
  }
4089
+ // -----------------------------------------------------------------------
4090
+ // Tool Discovery & Policy
4091
+ // -----------------------------------------------------------------------
3899
4092
  /**
3900
- * 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:
3901
4095
  *
3902
- * Two lists are always published together:
3903
- * - `tools` policy-filtered list that agents are allowed to call.
3904
- * - `allTools` — the complete, unfiltered list used by the management UI so
3905
- * 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)
3906
4098
  *
3907
- * `fetchTools()` is called first (populates the in-memory cache), then
3908
- * `gateway.listTools()` re-uses that cache internally — so only one remote
3909
- * 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.
3910
4101
  *
3911
- * @param sessionId - The session whose tools should be discovered.
3912
- * @returns The session record and the policy-filtered tool list.
3913
- * @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.
3914
4105
  */
3915
4106
  async listPolicyFilteredTools(sessionId) {
3916
- const session = await sessions.get(this.userId, sessionId);
3917
- if (!session) {
3918
- throw new Error("Session not found");
3919
- }
4107
+ const session = await this.requireSession(sessionId);
3920
4108
  const client = await this.getOrCreateClient(sessionId);
3921
4109
  const allTools = await client.fetchTools().catch(() => ({ tools: [] }));
3922
4110
  const gateway = createToolPolicyGateway(this.userId, sessionId, client);
3923
4111
  const result = await gateway.listTools();
3924
- this.emitConnectionEvent({
4112
+ this.sendEvent({
3925
4113
  type: "tools_discovered",
3926
4114
  sessionId,
3927
4115
  serverId: session.serverId ?? "unknown",
@@ -3933,254 +4121,254 @@ var SSEConnectionManager = class {
3933
4121
  return { session, result };
3934
4122
  }
3935
4123
  /**
3936
- * Returns the policy-filtered tool list for a session (agent-facing).
3937
- * Internally re-uses `listPolicyFilteredTools` which also emits a
3938
- * `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.
3939
4128
  */
3940
4129
  async listTools(params) {
3941
- const { sessionId } = params;
3942
- const { result } = await this.listPolicyFilteredTools(sessionId);
4130
+ const { result } = await this.listPolicyFilteredTools(params.sessionId);
3943
4131
  return { tools: result.tools };
3944
4132
  }
3945
4133
  /**
3946
- * 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.
3947
4136
  */
3948
4137
  async getToolPolicy(params) {
3949
- const { sessionId } = params;
3950
- const session = await sessions.get(this.userId, sessionId);
3951
- if (!session) {
3952
- throw new Error("Session not found");
3953
- }
3954
- const client = await this.getOrCreateClient(sessionId);
4138
+ const session = await this.requireSession(params.sessionId);
4139
+ const client = await this.getOrCreateClient(params.sessionId);
3955
4140
  const allTools = await client.fetchTools();
3956
- const toolPolicy = session.toolPolicy ?? {
4141
+ const policy = session.toolPolicy ?? {
3957
4142
  mode: "all",
3958
4143
  toolIds: [],
3959
4144
  updatedAt: session.updatedAt ?? session.createdAt
3960
4145
  };
3961
4146
  const serverId = session.serverId ?? "unknown";
3962
- const tools = allTools.tools.map((tool) => ({
3963
- ...tool,
3964
- toolId: createToolId(serverId, tool.name),
3965
- 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)
3966
4151
  }));
3967
4152
  return {
3968
- toolPolicy,
4153
+ toolPolicy: policy,
3969
4154
  tools,
3970
4155
  toolCount: tools.length,
3971
- allowedToolCount: tools.filter((tool) => tool.allowed).length
4156
+ allowedToolCount: tools.filter((t) => t.allowed).length
3972
4157
  };
3973
4158
  }
3974
4159
  /**
3975
- * Persists a new tool access policy for a session and broadcasts the updated
3976
- * filtered tool list to all connected browser clients via a `tools_discovered` event.
3977
- *
3978
- * Both `tools` (policy-filtered) and `allTools` (complete list) are emitted
3979
- * so the management UI can immediately reflect the new checkbox states without
3980
- * 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.
3981
4162
  *
3982
- * @param params - Session ID and the new `{ mode, toolIds }` policy to apply.
3983
- * @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.
3984
4165
  */
3985
4166
  async setToolPolicy(params) {
3986
- const { sessionId } = params;
3987
- const session = await sessions.get(this.userId, sessionId);
3988
- if (!session) {
3989
- throw new Error("Session not found");
3990
- }
3991
- const client = await this.getOrCreateClient(sessionId);
4167
+ const session = await this.requireSession(params.sessionId);
4168
+ const client = await this.getOrCreateClient(params.sessionId);
3992
4169
  const allTools = await client.fetchTools();
3993
- const toolPolicy = normalizeToolPolicyForUpdate(params.toolPolicy);
3994
- validateToolPolicyAgainstTools(toolPolicy, allTools.tools, session.serverId);
3995
- await sessions.update(this.userId, sessionId, { toolPolicy });
3996
- const filteredTools = createToolPolicyGateway(this.userId, sessionId, client).filterTools({ ...session, toolPolicy }, allTools.tools);
3997
- 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({
3998
4179
  type: "tools_discovered",
3999
- sessionId,
4180
+ sessionId: params.sessionId,
4000
4181
  serverId: session.serverId ?? "unknown",
4001
- toolCount: filteredTools.length,
4002
- tools: filteredTools,
4182
+ toolCount: filtered.length,
4183
+ tools: filtered,
4003
4184
  allTools: allTools.tools,
4004
4185
  timestamp: Date.now()
4005
4186
  });
4006
- return {
4007
- success: true,
4008
- toolPolicy,
4009
- tools: filteredTools,
4010
- toolCount: filteredTools.length
4011
- };
4012
- }
4013
- /**
4014
- * Proxies a tool invocation to the remote MCP server.
4015
- * Resolves the client for the given session and delegates to the tool router.
4016
- */
4017
- async callTool(params) {
4018
- const { sessionId, toolName, toolArgs } = params;
4019
- const client = await this.getOrCreateClient(sessionId);
4020
- const result = await createToolPolicyGateway(this.userId, sessionId, client).callTool(toolName, toolArgs);
4021
- const meta = result._meta || {};
4022
- return {
4023
- ...result,
4024
- _meta: {
4025
- ...meta,
4026
- sessionId
4027
- }
4028
- };
4187
+ return { success: true, toolPolicy: policy, tools: filtered, toolCount: filtered.length };
4029
4188
  }
4030
4189
  /**
4031
- * 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.
4032
4199
  */
4033
- async getSession(params) {
4034
- const { sessionId } = params;
4035
- const session = await sessions.get(this.userId, sessionId);
4036
- if (!session) {
4037
- throw new Error("Session not found");
4038
- }
4039
- this.emitConnectionEvent({
4040
- type: "state_changed",
4041
- sessionId,
4042
- serverId: session.serverId ?? "unknown",
4043
- serverName: session.serverName ?? "Unknown",
4044
- serverUrl: session.serverUrl,
4045
- state: "VALIDATING",
4046
- previousState: "DISCONNECTED",
4047
- timestamp: Date.now()
4048
- });
4049
- try {
4050
- const clientMetadata = await this.getResolvedClientMetadata();
4051
- const client = new MCPClient({
4052
- userId: this.userId,
4053
- sessionId,
4054
- // These fields are optional in MCPClient, but when rehydrating a known
4055
- // stored session on the server we pass them explicitly to preserve the
4056
- // original transport/connection metadata instead of relying on lazy
4057
- // reloading during initialize().
4058
- serverId: session.serverId,
4059
- serverName: session.serverName,
4060
- serverUrl: session.serverUrl,
4061
- callbackUrl: session.callbackUrl,
4062
- transportType: session.transportType,
4063
- headers: session.headers,
4064
- ...clientMetadata
4065
- });
4066
- client.onConnectionEvent((event) => this.emitConnectionEvent(event));
4067
- client.onObservabilityEvent((event) => this.sendEvent(event));
4068
- await client.connect();
4069
- this.clients.set(sessionId, client);
4070
- const { result: tools } = await this.listPolicyFilteredTools(sessionId);
4071
- return { success: true, toolCount: tools.tools.length };
4072
- } catch (error) {
4073
- this.emitConnectionEvent({
4074
- type: "error",
4075
- sessionId,
4076
- serverId: session.serverId ?? "unknown",
4077
- error: error instanceof Error ? error.message : "Validation failed",
4078
- errorType: "validation",
4079
- timestamp: Date.now()
4080
- });
4081
- throw error;
4082
- }
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 };
4083
4204
  }
4205
+ // -----------------------------------------------------------------------
4206
+ // Tool Execution
4207
+ // -----------------------------------------------------------------------
4084
4208
  /**
4085
- * 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.
4086
4214
  */
4087
- async finishAuth(params) {
4088
- const { code } = params;
4089
- const oauthState = params.state;
4090
- const parsedState = parseOAuthState(oauthState);
4091
- const sessionId = parsedState?.sessionId || oauthState;
4092
- const session = await sessions.get(this.userId, sessionId);
4093
- if (!session) {
4094
- throw new Error("Session not found");
4095
- }
4096
- try {
4097
- const client = new MCPClient({
4098
- userId: this.userId,
4099
- sessionId,
4100
- // These fields are optional in MCPClient, but when rehydrating a known
4101
- // stored session on the server we pass them explicitly to preserve the
4102
- // original connection metadata instead of relying on lazy
4103
- // reloading during initialize().
4104
- serverId: session.serverId,
4105
- serverName: session.serverName,
4106
- serverUrl: session.serverUrl,
4107
- callbackUrl: session.callbackUrl,
4108
- // NOTE: transportType is intentionally omitted here.
4109
- // The session's stored transportType is a placeholder ('streamable-http')
4110
- // set before transport negotiation. Omitting it lets MCPClient auto-negotiate
4111
- // (try streamable_http → SSE fallback), which is critical for servers like
4112
- // Neon that only support SSE transport.
4113
- headers: session.headers
4114
- });
4115
- client.onConnectionEvent((event) => this.emitConnectionEvent(event));
4116
- await client.finishAuth(code, oauthState);
4117
- this.clients.set(sessionId, client);
4118
- const { result: tools } = await this.listPolicyFilteredTools(sessionId);
4119
- return { success: true, toolCount: tools.tools.length };
4120
- } catch (error) {
4121
- this.emitConnectionEvent({
4122
- type: "error",
4123
- sessionId,
4124
- serverId: session.serverId ?? "unknown",
4125
- error: error instanceof Error ? error.message : "OAuth completion failed",
4126
- errorType: "auth",
4127
- timestamp: Date.now()
4128
- });
4129
- throw error;
4130
- }
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 } };
4131
4223
  }
4132
- /**
4133
- * List prompts from a session
4134
- */
4224
+ // -----------------------------------------------------------------------
4225
+ // Prompts
4226
+ // -----------------------------------------------------------------------
4227
+ /** Lists all prompts exposed by the remote MCP server. */
4135
4228
  async listPrompts(params) {
4136
- const { sessionId } = params;
4137
- const client = await this.getOrCreateClient(sessionId);
4229
+ const client = await this.getOrCreateClient(params.sessionId);
4138
4230
  const result = await client.listPrompts();
4139
4231
  return { prompts: result.prompts };
4140
4232
  }
4141
- /**
4142
- * Get a specific prompt
4143
- */
4233
+ /** Retrieves a specific prompt by name with optional arguments. */
4144
4234
  async getPrompt(params) {
4145
- const { sessionId, name, args } = params;
4146
- const client = await this.getOrCreateClient(sessionId);
4147
- return await client.getPrompt(name, args);
4235
+ const client = await this.getOrCreateClient(params.sessionId);
4236
+ return client.getPrompt(params.name, params.args);
4148
4237
  }
4149
- /**
4150
- * List resources from a session
4151
- */
4238
+ // -----------------------------------------------------------------------
4239
+ // Resources
4240
+ // -----------------------------------------------------------------------
4241
+ /** Lists all resources exposed by the remote MCP server. */
4152
4242
  async listResources(params) {
4153
- const { sessionId } = params;
4154
- const client = await this.getOrCreateClient(sessionId);
4243
+ const client = await this.getOrCreateClient(params.sessionId);
4155
4244
  const result = await client.listResources();
4156
4245
  return { resources: result.resources };
4157
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
+ }
4158
4278
  /**
4159
- * 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.
4160
4281
  */
4161
- async readResource(params) {
4162
- const { sessionId, uri } = params;
4163
- const client = await this.getOrCreateClient(sessionId);
4164
- 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;
4294
+ }
4295
+ /**
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.
4301
+ */
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
+ });
4165
4320
  }
4166
4321
  /**
4167
- * Emit connection event
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).
4168
4326
  */
4169
- emitConnectionEvent(event) {
4170
- this.sendEvent(event);
4327
+ attachClientEvents(client) {
4328
+ client.onConnectionEvent((e) => this.sendEvent(e));
4329
+ client.onObservabilityEvent((e) => this.sendEvent(e));
4171
4330
  }
4172
4331
  /**
4173
- * Cleanup and close all connections
4332
+ * Registers the client in the in-memory cache and attaches its event
4333
+ * listeners to the unified observability channel.
4174
4334
  */
4175
- async dispose() {
4176
- this.isActive = false;
4177
- if (this.heartbeatTimer) {
4178
- clearInterval(this.heartbeatTimer);
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;
4179
4362
  }
4180
- await Promise.all(
4181
- Array.from(this.clients.values()).map((client) => client.disconnect())
4182
- );
4183
- 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);
4184
4372
  }
4185
4373
  };
4186
4374
  function createSSEHandler(options) {
@@ -4209,8 +4397,7 @@ function createSSEHandler(options) {
4209
4397
  });
4210
4398
  req.on("end", async () => {
4211
4399
  try {
4212
- const request = JSON.parse(body);
4213
- await manager.handleRequest(request);
4400
+ await manager.handleRequest(JSON.parse(body));
4214
4401
  } catch {
4215
4402
  }
4216
4403
  });
@@ -4423,6 +4610,9 @@ var SSEClient = class {
4423
4610
  async getToolPolicy(sessionId) {
4424
4611
  return this.sendRequest("getToolPolicy", { sessionId });
4425
4612
  }
4613
+ async updateSession(sessionId, enabled) {
4614
+ return this.sendRequest("updateSession", { sessionId, enabled });
4615
+ }
4426
4616
  async listTools(sessionId) {
4427
4617
  return this.sendRequest("listTools", { sessionId });
4428
4618
  }
@@ -6354,6 +6544,8 @@ exports.createNextMcpHandler = createNextMcpHandler;
6354
6544
  exports.createRegexSearchToolDefinition = createRegexSearchToolDefinition;
6355
6545
  exports.createSSEHandler = createSSEHandler;
6356
6546
  exports.createSearchToolDefinition = createSearchToolDefinition;
6547
+ exports.decryptObject = decryptObject;
6548
+ exports.encryptObject = encryptObject;
6357
6549
  exports.executeMetaTool = executeMetaTool;
6358
6550
  exports.findToolByName = findToolByName;
6359
6551
  exports.getToolUiResourceUri = getToolUiResourceUri;
@@ -6367,5 +6559,6 @@ exports.onSessionMutation = onSessionMutation;
6367
6559
  exports.resolveMetaToolProxy = resolveMetaToolProxy;
6368
6560
  exports.sanitizeServerLabel = sanitizeServerLabel;
6369
6561
  exports.sessions = sessions;
6562
+ exports.withDbObservability = withDbObservability;
6370
6563
  //# sourceMappingURL=index.js.map
6371
6564
  //# sourceMappingURL=index.js.map