@mcp-ts/sdk 2.5.3 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +3 -574
  2. package/dist/adapters/agui-adapter.d.mts +4 -4
  3. package/dist/adapters/agui-adapter.d.ts +4 -4
  4. package/dist/adapters/agui-middleware.d.mts +4 -4
  5. package/dist/adapters/agui-middleware.d.ts +4 -4
  6. package/dist/adapters/ai-adapter.d.mts +4 -4
  7. package/dist/adapters/ai-adapter.d.ts +4 -4
  8. package/dist/adapters/langchain-adapter.d.mts +4 -4
  9. package/dist/adapters/langchain-adapter.d.ts +4 -4
  10. package/dist/adapters/mastra-adapter.d.mts +3 -3
  11. package/dist/adapters/mastra-adapter.d.ts +3 -3
  12. package/dist/client/index.d.mts +2 -2
  13. package/dist/client/index.d.ts +2 -2
  14. package/dist/client/index.js +3 -0
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/index.mjs +3 -0
  17. package/dist/client/index.mjs.map +1 -1
  18. package/dist/client/react.d.mts +10 -18
  19. package/dist/client/react.d.ts +10 -18
  20. package/dist/client/react.js +17 -0
  21. package/dist/client/react.js.map +1 -1
  22. package/dist/client/react.mjs +17 -0
  23. package/dist/client/react.mjs.map +1 -1
  24. package/dist/client/vue.d.mts +4 -4
  25. package/dist/client/vue.d.ts +4 -4
  26. package/dist/client/vue.js +3 -0
  27. package/dist/client/vue.js.map +1 -1
  28. package/dist/client/vue.mjs +3 -0
  29. package/dist/client/vue.mjs.map +1 -1
  30. package/dist/{index-BRZO_JyI.d.mts → index-BrDkbut5.d.ts} +2 -1
  31. package/dist/{index-C3YGagi2.d.ts → index-DHQvlx4K.d.mts} +2 -1
  32. package/dist/index.d.mts +5 -5
  33. package/dist/index.d.ts +5 -5
  34. package/dist/index.js +1056 -876
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +1048 -871
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{multi-session-client-CuacvZaQ.d.ts → multi-session-client-CQzymvHl.d.ts} +183 -227
  39. package/dist/{multi-session-client-DqzT8Oo7.d.mts → multi-session-client-C_Ja2TuV.d.mts} +183 -227
  40. package/dist/server/index.d.mts +338 -88
  41. package/dist/server/index.d.ts +338 -88
  42. package/dist/server/index.js +1053 -876
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +1045 -871
  45. package/dist/server/index.mjs.map +1 -1
  46. package/dist/shared/index.d.mts +4 -4
  47. package/dist/shared/index.d.ts +4 -4
  48. package/dist/shared/index.js.map +1 -1
  49. package/dist/shared/index.mjs.map +1 -1
  50. package/dist/{tool-router-B0qpui-V.d.mts → tool-router-C1n7OXbl.d.mts} +1 -1
  51. package/dist/{tool-router-2qUyZ-cP.d.ts → tool-router-CBIawFnt.d.ts} +1 -1
  52. package/dist/{types-BkJ_rgzo.d.mts → types-DX71u9gR.d.mts} +11 -5
  53. package/dist/{types-BkJ_rgzo.d.ts → types-DX71u9gR.d.ts} +11 -5
  54. package/migrations/neon/20260513010000_install_mcp_sessions.sql +4 -34
  55. package/migrations/supabase/20260330195700_install_mcp_sessions.sql +5 -62
  56. package/package.json +2 -1
  57. package/src/client/core/sse-client.ts +5 -0
  58. package/src/client/react/use-mcp.ts +27 -28
  59. package/src/server/handlers/sse-handler.ts +671 -633
  60. package/src/server/index.ts +2 -1
  61. package/src/server/mcp/multi-session-client.ts +34 -10
  62. package/src/server/mcp/oauth-client.ts +197 -205
  63. package/src/server/mcp/storage-oauth-provider.ts +148 -41
  64. package/src/server/mcp/tool-policy-gateway.ts +11 -1
  65. package/src/server/storage/file-backend.ts +25 -21
  66. package/src/server/storage/index.ts +71 -0
  67. package/src/server/storage/memory-backend.ts +24 -12
  68. package/src/server/storage/neon-backend.ts +70 -72
  69. package/src/server/storage/redis-backend.ts +26 -36
  70. package/src/server/storage/sqlite-backend.ts +25 -39
  71. package/src/server/storage/supabase-backend.ts +45 -54
  72. package/src/server/storage/types.ts +22 -2
  73. package/src/shared/types.ts +13 -3
package/dist/index.mjs CHANGED
@@ -4,6 +4,7 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
4
4
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
5
5
  import { UnauthorizedError as UnauthorizedError$1 } from '@modelcontextprotocol/sdk/client/auth.js';
6
6
  import { ListToolsResultSchema, CallToolResultSchema, ListPromptsResultSchema, GetPromptResultSchema, ListResourcesResultSchema, ReadResourceResultSchema } from '@modelcontextprotocol/sdk/types.js';
7
+ import { AsyncLocalStorage } from 'async_hooks';
7
8
  import * as fs2 from 'fs';
8
9
  import { promises } from 'fs';
9
10
  import * as path from 'path';
@@ -277,7 +278,6 @@ var RedisStorageBackend = class {
277
278
  constructor(redis2) {
278
279
  this.redis = redis2;
279
280
  __publicField(this, "KEY_PREFIX", "mcp:session:");
280
- __publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
281
281
  __publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
282
282
  __publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
283
283
  }
@@ -296,9 +296,6 @@ var RedisStorageBackend = class {
296
296
  getSessionKey(userId, sessionId) {
297
297
  return `${this.KEY_PREFIX}${userId}:${sessionId}`;
298
298
  }
299
- getCredentialsKey(userId, sessionId) {
300
- return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
301
- }
302
299
  /**
303
300
  * Generates Redis key for tracking all sessions for a user
304
301
  * @private
@@ -388,48 +385,54 @@ var RedisStorageBackend = class {
388
385
  }
389
386
  async patchCredentials(userId, sessionId, data) {
390
387
  const sessionKey = this.getSessionKey(userId, sessionId);
391
- const credentialsKey = this.getCredentialsKey(userId, sessionId);
392
- const sessionExists = await this.redis.exists(sessionKey);
393
- if (!sessionExists) {
388
+ const currentStr = await this.redis.get(sessionKey);
389
+ if (!currentStr) {
394
390
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
395
391
  }
396
- const session = await this.get(userId, sessionId);
397
- const currentTtl = await this.redis.ttl(sessionKey);
398
- const effectiveTtl = currentTtl > 0 && session ? currentTtl : resolveSessionRedisTtlSeconds(session ?? { status: "pending" });
399
- const currentStr = await this.redis.get(credentialsKey);
400
- const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
401
- const credentials = { ...current, ...data, sessionId, userId };
402
- if (effectiveTtl > 0) {
403
- await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
404
- } else {
405
- await this.redis.set(credentialsKey, JSON.stringify(credentials));
406
- }
392
+ const current = JSON.parse(currentStr);
393
+ const updated = { ...current, ...data, sessionId, userId };
394
+ const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
395
+ await this.redis.set(sessionKey, JSON.stringify(updated), "EX", effectiveTtl);
407
396
  }
408
- async get(userId, sessionId) {
397
+ async get(userId, sessionId, options) {
409
398
  try {
410
399
  const sessionKey = this.getSessionKey(userId, sessionId);
411
400
  const sessionDataStr = await this.redis.get(sessionKey);
412
- if (!sessionDataStr) {
413
- return null;
401
+ if (!sessionDataStr) return null;
402
+ const session = normalizeStoredSession(JSON.parse(sessionDataStr));
403
+ if (!options?.includeCredentials) {
404
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
405
+ return sessionOnly;
414
406
  }
415
- const session = JSON.parse(sessionDataStr);
416
- return normalizeStoredSession(session);
407
+ return session;
417
408
  } catch (error) {
418
409
  console.error("[RedisStorageBackend] Failed to get session:", error);
419
410
  return null;
420
411
  }
421
412
  }
422
413
  async getCredentials(userId, sessionId) {
423
- const session = await this.get(userId, sessionId);
414
+ const session = await this.get(userId, sessionId, { includeCredentials: true });
424
415
  if (!session) return null;
425
- const credentialsStr = await this.redis.get(this.getCredentialsKey(userId, sessionId));
426
- return credentialsStr ? JSON.parse(credentialsStr) : { sessionId, userId };
416
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
417
+ return {
418
+ sessionId,
419
+ userId,
420
+ clientInformation,
421
+ tokens,
422
+ codeVerifier,
423
+ codeVerifierChallenge,
424
+ codeVerifierNonce,
425
+ clientId,
426
+ oauthState
427
+ };
427
428
  }
428
429
  async clearCredentials(userId, sessionId) {
429
430
  await this.patchCredentials(userId, sessionId, {
430
431
  clientInformation: null,
431
432
  tokens: null,
432
433
  codeVerifier: null,
434
+ codeVerifierChallenge: null,
435
+ codeVerifierNonce: null,
433
436
  clientId: null,
434
437
  oauthState: null
435
438
  });
@@ -462,10 +465,9 @@ var RedisStorageBackend = class {
462
465
  async delete(userId, sessionId) {
463
466
  try {
464
467
  const sessionKey = this.getSessionKey(userId, sessionId);
465
- const credentialsKey = this.getCredentialsKey(userId, sessionId);
466
468
  const userIdKey = this.getUserIdKey(userId);
467
469
  await this.redis.srem(userIdKey, sessionId);
468
- await this.redis.del(sessionKey, credentialsKey);
470
+ await this.redis.del(sessionKey);
469
471
  } catch (error) {
470
472
  console.error("[RedisStorageBackend] Failed to remove session:", error);
471
473
  }
@@ -496,9 +498,8 @@ var RedisStorageBackend = class {
496
498
  async clearAll() {
497
499
  try {
498
500
  const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
499
- const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
500
501
  const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
501
- const allKeys = [...keys, ...credentialKeys, ...userIdKeys];
502
+ const allKeys = [...keys, ...userIdKeys];
502
503
  if (allKeys.length > 0) {
503
504
  await this.redis.del(...allKeys);
504
505
  }
@@ -522,7 +523,6 @@ var RedisStorageBackend = class {
522
523
  const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
523
524
  if (staleSessionIds.length > 0) {
524
525
  await this.redis.srem(userIdKey, ...staleSessionIds);
525
- await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
526
526
  }
527
527
  const remainingCount = await this.redis.scard(userIdKey);
528
528
  if (remainingCount === 0) {
@@ -547,7 +547,6 @@ var MemoryStorageBackend = class {
547
547
  constructor() {
548
548
  // Map<userId:sessionId, Session>
549
549
  __publicField(this, "sessions", /* @__PURE__ */ new Map());
550
- __publicField(this, "credentials", /* @__PURE__ */ new Map());
551
550
  // Map<userId, Set<sessionId>>
552
551
  __publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
553
552
  }
@@ -585,26 +584,46 @@ var MemoryStorageBackend = class {
585
584
  }
586
585
  async patchCredentials(userId, sessionId, data) {
587
586
  const sessionKey = this.getSessionKey(userId, sessionId);
588
- if (!this.sessions.has(sessionKey)) {
587
+ const current = this.sessions.get(sessionKey);
588
+ if (!current) {
589
589
  throw new Error(`Session ${sessionId} not found`);
590
590
  }
591
- const current = this.credentials.get(sessionKey) ?? { sessionId, userId };
592
- this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
591
+ this.sessions.set(sessionKey, { ...current, ...data, sessionId, userId });
593
592
  }
594
- async get(userId, sessionId) {
593
+ async get(userId, sessionId, options) {
595
594
  const sessionKey = this.getSessionKey(userId, sessionId);
596
- return this.sessions.get(sessionKey) || null;
595
+ const session = this.sessions.get(sessionKey) || null;
596
+ if (!session) return session;
597
+ if (!options?.includeCredentials) {
598
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
599
+ return sessionOnly;
600
+ }
601
+ return session;
597
602
  }
598
603
  async getCredentials(userId, sessionId) {
599
604
  const sessionKey = this.getSessionKey(userId, sessionId);
600
- if (!this.sessions.has(sessionKey)) return null;
601
- return this.credentials.get(sessionKey) ?? { sessionId, userId };
605
+ const session = this.sessions.get(sessionKey);
606
+ if (!session) return null;
607
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
608
+ return {
609
+ sessionId,
610
+ userId,
611
+ clientInformation,
612
+ tokens,
613
+ codeVerifier,
614
+ codeVerifierChallenge,
615
+ codeVerifierNonce,
616
+ clientId,
617
+ oauthState
618
+ };
602
619
  }
603
620
  async clearCredentials(userId, sessionId) {
604
621
  await this.patchCredentials(userId, sessionId, {
605
622
  clientInformation: null,
606
623
  tokens: null,
607
624
  codeVerifier: null,
625
+ codeVerifierChallenge: null,
626
+ codeVerifierNonce: null,
608
627
  clientId: null,
609
628
  oauthState: null
610
629
  });
@@ -628,7 +647,6 @@ var MemoryStorageBackend = class {
628
647
  async delete(userId, sessionId) {
629
648
  const sessionKey = this.getSessionKey(userId, sessionId);
630
649
  this.sessions.delete(sessionKey);
631
- this.credentials.delete(sessionKey);
632
650
  const set = this.userIdSessions.get(userId);
633
651
  if (set) {
634
652
  set.delete(sessionId);
@@ -642,14 +660,12 @@ var MemoryStorageBackend = class {
642
660
  }
643
661
  async clearAll() {
644
662
  this.sessions.clear();
645
- this.credentials.clear();
646
663
  this.userIdSessions.clear();
647
664
  }
648
665
  async cleanupExpired() {
649
666
  for (const [key, session] of this.sessions.entries()) {
650
667
  if (!isSessionExpired(session)) continue;
651
668
  this.sessions.delete(key);
652
- this.credentials.delete(key);
653
669
  const set = this.userIdSessions.get(session.userId);
654
670
  if (set) {
655
671
  set.delete(session.sessionId);
@@ -669,7 +685,6 @@ var FileStorageBackend = class {
669
685
  constructor(options = {}) {
670
686
  __publicField(this, "filePath");
671
687
  __publicField(this, "memoryCache", null);
672
- __publicField(this, "credentialsCache", null);
673
688
  __publicField(this, "initialized", false);
674
689
  this.filePath = options.path || "./sessions.json";
675
690
  }
@@ -684,22 +699,15 @@ var FileStorageBackend = class {
684
699
  const data = await promises.readFile(this.filePath, "utf-8");
685
700
  const json = JSON.parse(data);
686
701
  this.memoryCache = /* @__PURE__ */ new Map();
687
- this.credentialsCache = /* @__PURE__ */ new Map();
688
702
  if (Array.isArray(json.sessions)) {
689
703
  json.sessions.forEach((s) => {
690
704
  const session = normalizeStoredSession(s);
691
705
  this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
692
706
  });
693
707
  }
694
- if (Array.isArray(json.credentials)) {
695
- json.credentials.forEach((c) => {
696
- this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
697
- });
698
- }
699
708
  } catch (error) {
700
709
  if (error.code === "ENOENT") {
701
710
  this.memoryCache = /* @__PURE__ */ new Map();
702
- this.credentialsCache = /* @__PURE__ */ new Map();
703
711
  await this.flush();
704
712
  } else {
705
713
  console.error("[FileStorage] Failed to load sessions:", error);
@@ -713,10 +721,9 @@ var FileStorageBackend = class {
713
721
  if (!this.initialized) await this.init();
714
722
  }
715
723
  async flush() {
716
- if (!this.memoryCache || !this.credentialsCache) return;
724
+ if (!this.memoryCache) return;
717
725
  await promises.writeFile(this.filePath, JSON.stringify({
718
- sessions: Array.from(this.memoryCache.values()),
719
- credentials: Array.from(this.credentialsCache.values())
726
+ sessions: Array.from(this.memoryCache.values())
720
727
  }, null, 2), "utf-8");
721
728
  }
722
729
  getSessionKey(userId, sessionId) {
@@ -751,29 +758,49 @@ var FileStorageBackend = class {
751
758
  async patchCredentials(userId, sessionId, data) {
752
759
  await this.ensureInitialized();
753
760
  const sessionKey = this.getSessionKey(userId, sessionId);
754
- if (!this.memoryCache.has(sessionKey)) {
761
+ const current = this.memoryCache.get(sessionKey);
762
+ if (!current) {
755
763
  throw new Error(`Session ${sessionId} not found`);
756
764
  }
757
- const current = this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
758
- this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
765
+ this.memoryCache.set(sessionKey, { ...current, ...data, sessionId, userId });
759
766
  await this.flush();
760
767
  }
761
- async get(userId, sessionId) {
768
+ async get(userId, sessionId, options) {
762
769
  await this.ensureInitialized();
763
770
  const sessionKey = this.getSessionKey(userId, sessionId);
764
- return this.memoryCache.get(sessionKey) || null;
771
+ const session = this.memoryCache.get(sessionKey) || null;
772
+ if (!session) return null;
773
+ if (!options?.includeCredentials) {
774
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
775
+ return sessionOnly;
776
+ }
777
+ return session;
765
778
  }
766
779
  async getCredentials(userId, sessionId) {
767
780
  await this.ensureInitialized();
768
781
  const sessionKey = this.getSessionKey(userId, sessionId);
769
- if (!this.memoryCache.has(sessionKey)) return null;
770
- return this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
782
+ const session = this.memoryCache.get(sessionKey);
783
+ if (!session) return null;
784
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
785
+ return {
786
+ sessionId,
787
+ userId,
788
+ clientInformation,
789
+ tokens,
790
+ codeVerifier,
791
+ codeVerifierChallenge,
792
+ codeVerifierNonce,
793
+ clientId,
794
+ oauthState
795
+ };
771
796
  }
772
797
  async clearCredentials(userId, sessionId) {
773
798
  await this.patchCredentials(userId, sessionId, {
774
799
  clientInformation: null,
775
800
  tokens: null,
776
801
  codeVerifier: null,
802
+ codeVerifierChallenge: null,
803
+ codeVerifierNonce: null,
777
804
  clientId: null,
778
805
  oauthState: null
779
806
  });
@@ -790,7 +817,6 @@ var FileStorageBackend = class {
790
817
  await this.ensureInitialized();
791
818
  const sessionKey = this.getSessionKey(userId, sessionId);
792
819
  const deleted = this.memoryCache.delete(sessionKey);
793
- this.credentialsCache.delete(sessionKey);
794
820
  if (deleted) {
795
821
  await this.flush();
796
822
  }
@@ -802,7 +828,6 @@ var FileStorageBackend = class {
802
828
  async clearAll() {
803
829
  await this.ensureInitialized();
804
830
  this.memoryCache.clear();
805
- this.credentialsCache.clear();
806
831
  await this.flush();
807
832
  }
808
833
  async cleanupExpired() {
@@ -811,7 +836,6 @@ var FileStorageBackend = class {
811
836
  for (const [key, session] of this.memoryCache.entries()) {
812
837
  if (!isSessionExpired(session)) continue;
813
838
  this.memoryCache.delete(key);
814
- this.credentialsCache.delete(key);
815
839
  changed = true;
816
840
  }
817
841
  if (changed) {
@@ -825,12 +849,10 @@ var SqliteStorage = class {
825
849
  constructor(options = {}) {
826
850
  __publicField(this, "db", null);
827
851
  __publicField(this, "table");
828
- __publicField(this, "credentialsTable");
829
852
  __publicField(this, "initialized", false);
830
853
  __publicField(this, "dbPath");
831
854
  this.dbPath = options.path || "./sessions.db";
832
855
  this.table = options.table || "mcp_sessions";
833
- this.credentialsTable = `${this.table}_credentials`;
834
856
  }
835
857
  async init() {
836
858
  if (this.initialized) return;
@@ -850,13 +872,6 @@ var SqliteStorage = class {
850
872
  expiresAt INTEGER
851
873
  );
852
874
  CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
853
- CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
854
- sessionId TEXT NOT NULL,
855
- userId TEXT NOT NULL,
856
- data TEXT NOT NULL,
857
- PRIMARY KEY (userId, sessionId),
858
- FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
859
- );
860
875
  `);
861
876
  this.initialized = true;
862
877
  console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
@@ -913,41 +928,54 @@ var SqliteStorage = class {
913
928
  }
914
929
  async patchCredentials(userId, sessionId, data) {
915
930
  this.ensureInitialized();
916
- if (!await this.get(userId, sessionId)) {
931
+ const currentSession = await this.get(userId, sessionId, { includeCredentials: true });
932
+ if (!currentSession) {
917
933
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
918
934
  }
919
- const current = await this.getCredentials(userId, sessionId) ?? { sessionId, userId };
920
- const credentials = { ...current, ...data, sessionId, userId };
935
+ const updated = { ...currentSession, ...data, sessionId, userId };
921
936
  const stmt = this.db.prepare(
922
- `INSERT INTO ${this.credentialsTable} (sessionId, userId, data)
923
- VALUES (?, ?, ?)
924
- ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
937
+ `UPDATE ${this.table} SET data = ? WHERE sessionId = ? AND userId = ?`
925
938
  );
926
- stmt.run(sessionId, userId, JSON.stringify(credentials));
939
+ stmt.run(JSON.stringify(updated), sessionId, userId);
927
940
  }
928
- async get(userId, sessionId) {
941
+ async get(userId, sessionId, options) {
929
942
  this.ensureInitialized();
930
943
  const stmt = this.db.prepare(
931
944
  `SELECT data FROM ${this.table} WHERE sessionId = ? AND userId = ?`
932
945
  );
933
946
  const row = stmt.get(sessionId, userId);
934
947
  if (!row) return null;
935
- return normalizeStoredSession(JSON.parse(row.data));
948
+ const session = normalizeStoredSession(JSON.parse(row.data));
949
+ if (!options?.includeCredentials) {
950
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
951
+ return sessionOnly;
952
+ }
953
+ return session;
936
954
  }
937
955
  async getCredentials(userId, sessionId) {
938
956
  this.ensureInitialized();
939
- if (!await this.get(userId, sessionId)) return null;
940
- const stmt = this.db.prepare(
941
- `SELECT data FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
942
- );
943
- const row = stmt.get(sessionId, userId);
944
- return row ? JSON.parse(row.data) : { sessionId, userId };
957
+ const session = await this.get(userId, sessionId, { includeCredentials: true });
958
+ if (!session) return null;
959
+ const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
960
+ return {
961
+ sessionId,
962
+ userId,
963
+ clientInformation,
964
+ tokens,
965
+ codeVerifier,
966
+ codeVerifierChallenge,
967
+ codeVerifierNonce,
968
+ clientId,
969
+ oauthState
970
+ };
945
971
  }
946
972
  async clearCredentials(userId, sessionId) {
947
973
  await this.patchCredentials(userId, sessionId, {
948
974
  clientInformation: null,
949
975
  tokens: null,
950
976
  codeVerifier: null,
977
+ codeVerifierChallenge: null,
978
+ codeVerifierNonce: null,
951
979
  clientId: null,
952
980
  oauthState: null
953
981
  });
@@ -973,9 +1001,6 @@ var SqliteStorage = class {
973
1001
  const stmt = this.db.prepare(
974
1002
  `DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
975
1003
  );
976
- this.db.prepare(
977
- `DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
978
- ).run(sessionId, userId);
979
1004
  stmt.run(sessionId, userId);
980
1005
  }
981
1006
  async listAllIds() {
@@ -986,7 +1011,6 @@ var SqliteStorage = class {
986
1011
  }
987
1012
  async clearAll() {
988
1013
  this.ensureInitialized();
989
- this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
990
1014
  const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
991
1015
  stmt.run();
992
1016
  }
@@ -1000,14 +1024,6 @@ var SqliteStorage = class {
1000
1024
  deleteStmt.run(row.sessionId, row.userId);
1001
1025
  }
1002
1026
  }
1003
- this.db.prepare(
1004
- `DELETE FROM ${this.credentialsTable}
1005
- WHERE NOT EXISTS (
1006
- SELECT 1 FROM ${this.table}
1007
- WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
1008
- AND ${this.table}.userId = ${this.credentialsTable}.userId
1009
- )`
1010
- ).run();
1011
1027
  }
1012
1028
  async disconnect() {
1013
1029
  if (this.db) {
@@ -1089,7 +1105,6 @@ var SupabaseStorageBackend = class {
1089
1105
  }
1090
1106
  async init() {
1091
1107
  await this.assertTable("mcp_sessions", "session_id");
1092
- await this.assertTable("mcp_credentials", "session_id");
1093
1108
  console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
1094
1109
  }
1095
1110
  async assertTable(table, column) {
@@ -1120,18 +1135,13 @@ var SupabaseStorageBackend = class {
1120
1135
  headers: decryptObject(row.headers),
1121
1136
  authUrl: row.auth_url,
1122
1137
  status: row.status ?? "pending",
1123
- toolPolicy: normalizeToolPolicy(row.tool_policy)
1124
- };
1125
- }
1126
- mapRowToCredentials(row, userId, sessionId) {
1127
- return {
1128
- sessionId,
1129
- userId,
1130
- clientInformation: decryptObject(row?.client_information),
1131
- tokens: decryptObject(row?.tokens),
1132
- codeVerifier: decryptObject(row?.code_verifier),
1133
- clientId: row?.client_id,
1134
- oauthState: row?.oauth_state
1138
+ toolPolicy: normalizeToolPolicy(row.tool_policy),
1139
+ clientInformation: decryptObject(row.client_information),
1140
+ tokens: decryptObject(row.tokens),
1141
+ codeVerifier: decryptObject(row.code_verifier),
1142
+ clientId: row.client_id,
1143
+ oauthState: row.oauth_state,
1144
+ enabled: row.enabled ?? true
1135
1145
  };
1136
1146
  }
1137
1147
  hasCredentialData(data) {
@@ -1189,6 +1199,7 @@ var SupabaseStorageBackend = class {
1189
1199
  if ("headers" in data) updateData.headers = encryptObject(data.headers);
1190
1200
  if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
1191
1201
  if ("toolPolicy" in data) updateData.tool_policy = normalizeToolPolicy(data.toolPolicy);
1202
+ if ("enabled" in data) updateData.enabled = data.enabled;
1192
1203
  const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
1193
1204
  let updatedRows = null;
1194
1205
  if (shouldUpdateSession) {
@@ -1210,23 +1221,22 @@ var SupabaseStorageBackend = class {
1210
1221
  }
1211
1222
  async patchCredentials(userId, sessionId, data) {
1212
1223
  if (!this.hasCredentialData(data)) return;
1213
- const row = {
1214
- user_id: userId,
1215
- session_id: sessionId,
1224
+ const updateData = {
1216
1225
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
1217
1226
  };
1218
- if ("clientInformation" in data) row.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
1219
- if ("tokens" in data) row.tokens = data.tokens == null ? null : encryptObject(data.tokens);
1220
- if ("codeVerifier" in data) row.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
1221
- if ("clientId" in data) row.client_id = data.clientId ?? null;
1222
- if ("oauthState" in data) row.oauth_state = data.oauthState ?? null;
1223
- const { error } = await this.supabase.from("mcp_credentials").upsert(row, { onConflict: "user_id,session_id" });
1227
+ if ("clientInformation" in data) updateData.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
1228
+ if ("tokens" in data) updateData.tokens = data.tokens == null ? null : encryptObject(data.tokens);
1229
+ if ("codeVerifier" in data) updateData.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
1230
+ if ("clientId" in data) updateData.client_id = data.clientId ?? null;
1231
+ if ("oauthState" in data) updateData.oauth_state = data.oauthState ?? null;
1232
+ const { error } = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId);
1224
1233
  if (error) {
1225
1234
  throw new Error(`Failed to update credentials: ${error.message}`);
1226
1235
  }
1227
1236
  }
1228
- async get(userId, sessionId) {
1229
- const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
1237
+ async get(userId, sessionId, options) {
1238
+ 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";
1239
+ const { data, error } = await this.supabase.from("mcp_sessions").select(selection).eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
1230
1240
  if (error) {
1231
1241
  console.error("[SupabaseStorage] Failed to get session:", error);
1232
1242
  return null;
@@ -1235,19 +1245,21 @@ var SupabaseStorageBackend = class {
1235
1245
  return this.mapRowToSessionData(data);
1236
1246
  }
1237
1247
  async getCredentials(userId, sessionId) {
1238
- const { data, error } = await this.supabase.from("mcp_credentials").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
1248
+ 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();
1239
1249
  if (error) {
1240
1250
  console.error("[SupabaseStorage] Failed to get credentials:", error);
1241
1251
  return null;
1242
1252
  }
1243
- if (data) {
1244
- return this.mapRowToCredentials(data, userId, sessionId);
1245
- }
1246
- const { data: sessionRows, error: sessionError } = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
1247
- if (sessionError || !sessionRows || sessionRows.length === 0) {
1248
- return null;
1249
- }
1250
- return { sessionId, userId };
1253
+ if (!data) return null;
1254
+ return {
1255
+ sessionId,
1256
+ userId,
1257
+ clientInformation: decryptObject(data.client_information),
1258
+ tokens: decryptObject(data.tokens),
1259
+ codeVerifier: decryptObject(data.code_verifier),
1260
+ clientId: data.client_id,
1261
+ oauthState: data.oauth_state
1262
+ };
1251
1263
  }
1252
1264
  async list(userId) {
1253
1265
  const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
@@ -1258,7 +1270,14 @@ var SupabaseStorageBackend = class {
1258
1270
  return data.map((row) => this.mapRowToSessionData(row));
1259
1271
  }
1260
1272
  async clearCredentials(userId, sessionId) {
1261
- const { error } = await this.supabase.from("mcp_credentials").delete().eq("user_id", userId).eq("session_id", sessionId);
1273
+ const { error } = await this.supabase.from("mcp_sessions").update({
1274
+ client_information: null,
1275
+ tokens: null,
1276
+ code_verifier: null,
1277
+ client_id: null,
1278
+ oauth_state: null,
1279
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1280
+ }).eq("user_id", userId).eq("session_id", sessionId);
1262
1281
  if (error) {
1263
1282
  throw new Error(`Failed to clear credentials: ${error.message}`);
1264
1283
  }
@@ -1286,10 +1305,6 @@ var SupabaseStorageBackend = class {
1286
1305
  return data.map((row) => row.session_id);
1287
1306
  }
1288
1307
  async clearAll() {
1289
- const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
1290
- if (credentialsError) {
1291
- console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
1292
- }
1293
1308
  const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
1294
1309
  if (error) {
1295
1310
  console.error("[SupabaseStorage] Failed to clear sessions:", error);
@@ -1315,16 +1330,12 @@ var NeonStorageBackend = class {
1315
1330
  constructor(sql, options = {}) {
1316
1331
  this.sql = sql;
1317
1332
  __publicField(this, "tableName");
1318
- __publicField(this, "credentialsTableName");
1319
1333
  const schema = options.schema || "public";
1320
1334
  const table = options.table || "mcp_sessions";
1321
- const credentialsTable = options.credentialsTable || "mcp_credentials";
1322
1335
  this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
1323
- this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
1324
1336
  }
1325
1337
  async init() {
1326
1338
  await this.assertTable(this.tableName, "mcp_sessions");
1327
- await this.assertTable(this.credentialsTableName, "mcp_credentials");
1328
1339
  console.log("[mcp-ts][Storage] Neon: storage tables verified.");
1329
1340
  }
1330
1341
  async assertTable(qualifiedName, displayName) {
@@ -1362,18 +1373,13 @@ var NeonStorageBackend = class {
1362
1373
  headers: decryptObject(row.headers),
1363
1374
  authUrl: row.auth_url ?? void 0,
1364
1375
  status: row.status ?? "pending",
1365
- toolPolicy: normalizeToolPolicy(row.tool_policy)
1366
- };
1367
- }
1368
- mapRowToCredentials(row, userId, sessionId) {
1369
- return {
1370
- sessionId,
1371
- userId,
1376
+ toolPolicy: normalizeToolPolicy(row.tool_policy),
1372
1377
  clientInformation: decryptObject(row.client_information),
1373
1378
  tokens: decryptObject(row.tokens),
1374
1379
  codeVerifier: decryptObject(row.code_verifier),
1375
1380
  clientId: row.client_id ?? void 0,
1376
- oauthState: row.oauth_state
1381
+ oauthState: row.oauth_state,
1382
+ enabled: row.enabled ?? true
1377
1383
  };
1378
1384
  }
1379
1385
  hasCredentialData(data) {
@@ -1443,7 +1449,7 @@ var NeonStorageBackend = class {
1443
1449
  const updatedSession = { ...currentSession, ...data };
1444
1450
  const status = updatedSession.status ?? "pending";
1445
1451
  const expiresAt = resolveSessionExpiresAt(status);
1446
- 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;
1452
+ 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;
1447
1453
  if (shouldUpdateSession) {
1448
1454
  const setClauses = [];
1449
1455
  const values = [];
@@ -1466,6 +1472,9 @@ var NeonStorageBackend = class {
1466
1472
  const toolPolicy = normalizeToolPolicy(updatedSession.toolPolicy, policyUpdatedAt) ?? { mode: "all", toolIds: [], updatedAt: policyUpdatedAt };
1467
1473
  addSet("tool_policy", toolPolicy);
1468
1474
  }
1475
+ if ("enabled" in data) {
1476
+ addSet("enabled", updatedSession.enabled);
1477
+ }
1469
1478
  setClauses.push("updated_at = now()");
1470
1479
  const updatedRows = await this.sql.query(
1471
1480
  `UPDATE ${this.tableName}
@@ -1481,45 +1490,45 @@ var NeonStorageBackend = class {
1481
1490
  }
1482
1491
  async patchCredentials(userId, sessionId, data) {
1483
1492
  if (!this.hasCredentialData(data)) return;
1484
- await this.sql.query(
1485
- `INSERT INTO ${this.credentialsTableName} (
1486
- user_id,
1487
- session_id,
1488
- client_information,
1489
- tokens,
1490
- code_verifier,
1491
- client_id,
1492
- oauth_state,
1493
- updated_at
1494
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, now())
1495
- ON CONFLICT (user_id, session_id)
1496
- DO UPDATE SET
1497
- client_information = CASE WHEN $8 THEN EXCLUDED.client_information ELSE ${this.credentialsTableName}.client_information END,
1498
- tokens = CASE WHEN $9 THEN EXCLUDED.tokens ELSE ${this.credentialsTableName}.tokens END,
1499
- code_verifier = CASE WHEN $10 THEN EXCLUDED.code_verifier ELSE ${this.credentialsTableName}.code_verifier END,
1500
- client_id = CASE WHEN $11 THEN EXCLUDED.client_id ELSE ${this.credentialsTableName}.client_id END,
1501
- oauth_state = CASE WHEN $12 THEN EXCLUDED.oauth_state ELSE ${this.credentialsTableName}.oauth_state END,
1502
- updated_at = now()`,
1503
- [
1504
- userId,
1505
- sessionId,
1506
- "clientInformation" in data ? data.clientInformation == null ? null : encryptObject(data.clientInformation) : null,
1507
- "tokens" in data ? data.tokens == null ? null : encryptObject(data.tokens) : null,
1508
- "codeVerifier" in data ? data.codeVerifier == null ? null : encryptObject(data.codeVerifier) : null,
1509
- "clientId" in data ? data.clientId ?? null : null,
1510
- "oauthState" in data ? data.oauthState ?? null : null,
1511
- "clientInformation" in data,
1512
- "tokens" in data,
1513
- "codeVerifier" in data,
1514
- "clientId" in data,
1515
- "oauthState" in data
1516
- ]
1493
+ const setClauses = [];
1494
+ const values = [];
1495
+ let paramIndex = 1;
1496
+ const addSet = (column, value) => {
1497
+ setClauses.push(`${column} = $${paramIndex++}`);
1498
+ values.push(value);
1499
+ };
1500
+ if ("clientInformation" in data) {
1501
+ addSet("client_information", data.clientInformation == null ? null : encryptObject(data.clientInformation));
1502
+ }
1503
+ if ("tokens" in data) {
1504
+ addSet("tokens", data.tokens == null ? null : encryptObject(data.tokens));
1505
+ }
1506
+ if ("codeVerifier" in data) {
1507
+ addSet("code_verifier", data.codeVerifier == null ? null : encryptObject(data.codeVerifier));
1508
+ }
1509
+ if ("clientId" in data) {
1510
+ addSet("client_id", data.clientId ?? null);
1511
+ }
1512
+ if ("oauthState" in data) {
1513
+ addSet("oauth_state", data.oauthState ?? null);
1514
+ }
1515
+ setClauses.push("updated_at = now()");
1516
+ const updatedRows = await this.sql.query(
1517
+ `UPDATE ${this.tableName}
1518
+ SET ${setClauses.join(", ")}
1519
+ WHERE user_id = $${paramIndex++} AND session_id = $${paramIndex++}
1520
+ RETURNING id`,
1521
+ [...values, userId, sessionId]
1517
1522
  );
1523
+ if (updatedRows.length === 0) {
1524
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
1525
+ }
1518
1526
  }
1519
- async get(userId, sessionId) {
1527
+ async get(userId, sessionId, options) {
1520
1528
  try {
1529
+ 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";
1521
1530
  const rows = await this.sql.query(
1522
- `SELECT * FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1531
+ `SELECT ${selection} FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1523
1532
  [userId, sessionId]
1524
1533
  );
1525
1534
  if (!rows[0]) return null;
@@ -1531,18 +1540,22 @@ var NeonStorageBackend = class {
1531
1540
  }
1532
1541
  async getCredentials(userId, sessionId) {
1533
1542
  try {
1534
- const credentialRows = await this.sql.query(
1535
- `SELECT * FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
1536
- [userId, sessionId]
1537
- );
1538
- if (credentialRows[0]) {
1539
- return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
1540
- }
1541
- const sessionRows = await this.sql.query(
1542
- `SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1543
+ const rows = await this.sql.query(
1544
+ `SELECT client_information, tokens, code_verifier, client_id, oauth_state
1545
+ FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
1543
1546
  [userId, sessionId]
1544
1547
  );
1545
- return sessionRows[0] ? { sessionId, userId } : null;
1548
+ if (!rows[0]) return null;
1549
+ const row = rows[0];
1550
+ return {
1551
+ sessionId,
1552
+ userId,
1553
+ clientInformation: decryptObject(row.client_information),
1554
+ tokens: decryptObject(row.tokens),
1555
+ codeVerifier: decryptObject(row.code_verifier),
1556
+ clientId: row.client_id ?? void 0,
1557
+ oauthState: row.oauth_state
1558
+ };
1546
1559
  } catch (error) {
1547
1560
  console.error("[NeonStorage] Failed to get credentials:", error);
1548
1561
  return null;
@@ -1563,7 +1576,9 @@ var NeonStorageBackend = class {
1563
1576
  async clearCredentials(userId, sessionId) {
1564
1577
  try {
1565
1578
  await this.sql.query(
1566
- `DELETE FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
1579
+ `UPDATE ${this.tableName}
1580
+ SET client_information = null, tokens = null, code_verifier = null, client_id = null, oauth_state = null, updated_at = now()
1581
+ WHERE user_id = $1 AND session_id = $2`,
1567
1582
  [userId, sessionId]
1568
1583
  );
1569
1584
  } catch (error) {
@@ -1605,7 +1620,6 @@ var NeonStorageBackend = class {
1605
1620
  }
1606
1621
  async clearAll() {
1607
1622
  try {
1608
- await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
1609
1623
  await this.sql.query(`DELETE FROM ${this.tableName}`);
1610
1624
  } catch (error) {
1611
1625
  console.error("[NeonStorage] Failed to clear sessions:", error);
@@ -1847,6 +1861,60 @@ function onSessionMutation(listener) {
1847
1861
  sessionMutationListeners.delete(listener);
1848
1862
  };
1849
1863
  }
1864
+ function withDbObservability(store, emit) {
1865
+ const readMethods = /* @__PURE__ */ new Set([
1866
+ "get",
1867
+ "getCredentials",
1868
+ "list",
1869
+ "listIds",
1870
+ "listAllIds"
1871
+ ]);
1872
+ return new Proxy(store, {
1873
+ get(target, prop, receiver) {
1874
+ const original = Reflect.get(target, prop, receiver);
1875
+ if (typeof original !== "function") return original;
1876
+ return (...args) => {
1877
+ const method = prop;
1878
+ const isRead = readMethods.has(prop);
1879
+ const start = performance.now();
1880
+ const emitEvent = (error) => {
1881
+ emit({
1882
+ type: isRead ? "db:read" : "db:write",
1883
+ level: error ? "error" : "debug",
1884
+ message: `${method}(${args.map(
1885
+ (a) => typeof a === "string" ? a.slice(0, 24) : typeof a
1886
+ ).join(", ")})`,
1887
+ sessionId: typeof args[1] === "string" ? args[1] : void 0,
1888
+ payload: {
1889
+ method,
1890
+ argTypes: args.map((a) => typeof a),
1891
+ durationMs: performance.now() - start,
1892
+ ...error ? { error } : {}
1893
+ },
1894
+ timestamp: Date.now()
1895
+ });
1896
+ };
1897
+ try {
1898
+ const result = original.apply(target, args);
1899
+ if (result instanceof Promise) {
1900
+ return result.then((r) => {
1901
+ emitEvent();
1902
+ return r;
1903
+ }).catch((e) => {
1904
+ emitEvent(String(e));
1905
+ throw e;
1906
+ });
1907
+ }
1908
+ emitEvent();
1909
+ return result;
1910
+ } catch (e) {
1911
+ emitEvent(String(e));
1912
+ throw e;
1913
+ }
1914
+ };
1915
+ }
1916
+ });
1917
+ }
1850
1918
  var sessions = new Proxy({}, {
1851
1919
  get(_target, prop) {
1852
1920
  return async (...args) => {
@@ -1866,6 +1934,24 @@ var sessions = new Proxy({}, {
1866
1934
  });
1867
1935
 
1868
1936
  // src/server/mcp/storage-oauth-provider.ts
1937
+ var codeVerifierContext = new AsyncLocalStorage();
1938
+ function runWithCodeVerifierState(verifier, method, fn) {
1939
+ return codeVerifierContext.run({ verifier, method }, fn);
1940
+ }
1941
+ function base64UrlEncode(bytes) {
1942
+ let binary = "";
1943
+ for (const byte of bytes) {
1944
+ binary += String.fromCharCode(byte);
1945
+ }
1946
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1947
+ }
1948
+ async function createCodeChallenge(verifier) {
1949
+ const digest = await crypto.subtle.digest(
1950
+ "SHA-256",
1951
+ new TextEncoder().encode(verifier)
1952
+ );
1953
+ return base64UrlEncode(new Uint8Array(digest));
1954
+ }
1869
1955
  var StorageOAuthClientProvider = class {
1870
1956
  /**
1871
1957
  * Creates a new session-backed OAuth provider
@@ -1881,8 +1967,12 @@ var StorageOAuthClientProvider = class {
1881
1967
  __publicField(this, "logoUri");
1882
1968
  __publicField(this, "policyUri");
1883
1969
  __publicField(this, "clientSecret");
1970
+ __publicField(this, "_store");
1884
1971
  __publicField(this, "_authUrl");
1885
1972
  __publicField(this, "_clientId");
1973
+ __publicField(this, "_cachedCodeVerifier");
1974
+ __publicField(this, "_hasCodeVerifier", false);
1975
+ __publicField(this, "_cachedTokens");
1886
1976
  __publicField(this, "onRedirectCallback");
1887
1977
  this.userId = options.userId;
1888
1978
  this.serverId = options.serverId;
@@ -1894,6 +1984,8 @@ var StorageOAuthClientProvider = class {
1894
1984
  this.policyUri = options.policyUri;
1895
1985
  this._clientId = options.clientId;
1896
1986
  this.clientSecret = options.clientSecret;
1987
+ this._cachedTokens = options.cachedTokens;
1988
+ this._store = options.sessionStore ?? sessions;
1897
1989
  this.onRedirectCallback = options.onRedirect;
1898
1990
  }
1899
1991
  get clientMetadata() {
@@ -1921,7 +2013,7 @@ var StorageOAuthClientProvider = class {
1921
2013
  * @private
1922
2014
  */
1923
2015
  async getCredentials() {
1924
- const data = await sessions.getCredentials(this.userId, this.sessionId);
2016
+ const data = await this._store.getCredentials(this.userId, this.sessionId);
1925
2017
  if (!data) {
1926
2018
  return { userId: this.userId, sessionId: this.sessionId };
1927
2019
  }
@@ -1934,35 +2026,33 @@ var StorageOAuthClientProvider = class {
1934
2026
  * @throws Error if session doesn't exist (session must be created by controller layer)
1935
2027
  */
1936
2028
  async patchCredentials(data) {
1937
- await sessions.patchCredentials(this.userId, this.sessionId, data);
2029
+ await this._store.patchCredentials(this.userId, this.sessionId, data);
1938
2030
  }
1939
2031
  /**
1940
- * Retrieves stored OAuth client information
2032
+ * Retrieves stored OAuth client information.
1941
2033
  */
1942
2034
  async clientInformation() {
1943
- const data = await this.getCredentials();
1944
- if (data.clientId && !this._clientId) {
1945
- this._clientId = data.clientId;
1946
- }
1947
- if (this._clientId && !data.clientId) {
1948
- await this.patchCredentials({
1949
- clientId: this._clientId,
1950
- clientInformation: {
1951
- client_id: this._clientId,
1952
- ...this.clientSecret ? { client_secret: this.clientSecret } : {}
1953
- }
1954
- });
2035
+ if (this._clientId) {
2036
+ return {
2037
+ client_id: this._clientId,
2038
+ ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2039
+ };
1955
2040
  }
1956
- if (data.clientInformation) {
1957
- return data.clientInformation;
2041
+ const data = await this.getCredentials();
2042
+ if (this._cachedTokens === void 0) {
2043
+ this._cachedTokens = data.tokens ?? null;
1958
2044
  }
1959
- if (!this._clientId) {
1960
- return void 0;
2045
+ if (data.clientId) {
2046
+ this._clientId = data.clientId;
2047
+ if (data.clientInformation) {
2048
+ return data.clientInformation;
2049
+ }
2050
+ return {
2051
+ client_id: data.clientId,
2052
+ ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2053
+ };
1961
2054
  }
1962
- return {
1963
- client_id: this._clientId,
1964
- ...this.clientSecret ? { client_secret: this.clientSecret } : {}
1965
- };
2055
+ return void 0;
1966
2056
  }
1967
2057
  /**
1968
2058
  * Stores OAuth client information
@@ -1979,11 +2069,30 @@ var StorageOAuthClientProvider = class {
1979
2069
  */
1980
2070
  async saveTokens(tokens) {
1981
2071
  await this.patchCredentials({ tokens });
2072
+ this._cachedTokens = tokens;
2073
+ }
2074
+ /**
2075
+ * Persists static client credentials to DB immediately on creation.
2076
+ * Ensures getOrCreateClient() finds them on rehydration, even when the
2077
+ * server allows anonymous connect and never triggers DCR.
2078
+ */
2079
+ async initializeCredentials() {
2080
+ if (this._clientId) {
2081
+ await this.patchCredentials({
2082
+ clientId: this._clientId,
2083
+ clientInformation: {
2084
+ client_id: this._clientId,
2085
+ ...this.clientSecret ? { client_secret: this.clientSecret } : {}
2086
+ }
2087
+ });
2088
+ }
1982
2089
  }
1983
2090
  get authUrl() {
1984
2091
  return this._authUrl;
1985
2092
  }
1986
2093
  async state() {
2094
+ this._cachedCodeVerifier = void 0;
2095
+ this._hasCodeVerifier = false;
1987
2096
  const nonce = nanoid(32);
1988
2097
  await this.patchCredentials({
1989
2098
  oauthState: {
@@ -2004,7 +2113,7 @@ var StorageOAuthClientProvider = class {
2004
2113
  if (parsed.sessionId !== this.sessionId) {
2005
2114
  return { valid: false, error: "OAuth state mismatch" };
2006
2115
  }
2007
- const data = await sessions.getCredentials(this.userId, parsed.sessionId);
2116
+ const data = await this._store.getCredentials(this.userId, parsed.sessionId);
2008
2117
  if (!data) {
2009
2118
  return { valid: false, error: "Session not found" };
2010
2119
  }
@@ -2029,49 +2138,77 @@ var StorageOAuthClientProvider = class {
2029
2138
  }
2030
2139
  async redirectToAuthorization(authUrl) {
2031
2140
  this._authUrl = authUrl.toString();
2032
- await sessions.update(this.userId, this.sessionId, { authUrl: this._authUrl });
2141
+ const codeChallenge = authUrl.searchParams.get("code_challenge");
2142
+ const state = authUrl.searchParams.get("state");
2143
+ if (this._cachedCodeVerifier && codeChallenge && state) {
2144
+ const expectedChallenge = await createCodeChallenge(this._cachedCodeVerifier);
2145
+ if (expectedChallenge === codeChallenge) {
2146
+ const parsed = parseOAuthState(state);
2147
+ if (parsed) {
2148
+ await this.patchCredentials({
2149
+ codeVerifier: this._cachedCodeVerifier
2150
+ });
2151
+ }
2152
+ }
2153
+ }
2033
2154
  if (this.onRedirectCallback) {
2034
2155
  this.onRedirectCallback(authUrl.toString());
2035
2156
  }
2036
2157
  }
2037
2158
  async invalidateCredentials(scope) {
2038
2159
  if (scope === "all") {
2039
- await sessions.delete(this.userId, this.sessionId);
2160
+ this._cachedTokens = void 0;
2161
+ await this._store.delete(this.userId, this.sessionId);
2040
2162
  } else {
2041
2163
  const updates = {};
2042
2164
  if (scope === "client") {
2043
2165
  updates.clientInformation = null;
2044
2166
  updates.clientId = null;
2045
2167
  } else if (scope === "tokens") {
2168
+ this._cachedTokens = void 0;
2046
2169
  updates.tokens = null;
2047
2170
  } else if (scope === "verifier") {
2171
+ this._cachedCodeVerifier = void 0;
2172
+ this._hasCodeVerifier = false;
2048
2173
  updates.codeVerifier = null;
2049
2174
  }
2050
2175
  await this.patchCredentials(updates);
2051
2176
  }
2052
2177
  }
2053
2178
  async saveCodeVerifier(verifier) {
2054
- const data = await this.getCredentials();
2055
- if (data.codeVerifier) {
2179
+ if (this._hasCodeVerifier) {
2056
2180
  return;
2057
2181
  }
2058
- await this.patchCredentials({ codeVerifier: verifier });
2182
+ this._cachedCodeVerifier = verifier;
2183
+ this._hasCodeVerifier = true;
2059
2184
  }
2060
2185
  async codeVerifier() {
2061
- const data = await this.getCredentials();
2062
- if (data.clientId && !this._clientId) {
2063
- this._clientId = data.clientId;
2186
+ if (this._cachedCodeVerifier) {
2187
+ return this._cachedCodeVerifier;
2064
2188
  }
2065
- if (!data.codeVerifier) {
2066
- throw new Error("No code verifier found");
2189
+ const ctx = codeVerifierContext.getStore();
2190
+ if (ctx?.verifier) {
2191
+ return ctx.verifier;
2192
+ }
2193
+ const data = await this.getCredentials();
2194
+ if (data.codeVerifier) {
2195
+ return data.codeVerifier;
2067
2196
  }
2068
- return data.codeVerifier;
2197
+ throw new Error("No code verifier found");
2069
2198
  }
2070
2199
  async deleteCodeVerifier() {
2071
- await this.patchCredentials({ codeVerifier: null });
2200
+ this._cachedCodeVerifier = void 0;
2201
+ this._hasCodeVerifier = false;
2202
+ await this.patchCredentials({
2203
+ codeVerifier: null
2204
+ });
2072
2205
  }
2073
2206
  async tokens() {
2207
+ if (this._cachedTokens !== void 0) {
2208
+ return this._cachedTokens ?? void 0;
2209
+ }
2074
2210
  const data = await this.getCredentials();
2211
+ this._cachedTokens = data.tokens ?? null;
2075
2212
  if (data.clientId && !this._clientId) {
2076
2213
  this._clientId = data.clientId;
2077
2214
  }
@@ -2232,23 +2369,10 @@ var MCPClient = class {
2232
2369
  __publicField(this, "client");
2233
2370
  __publicField(this, "oauthProvider", null);
2234
2371
  __publicField(this, "transport", null);
2235
- __publicField(this, "userId");
2236
- __publicField(this, "serverId");
2237
- __publicField(this, "sessionId");
2238
- __publicField(this, "serverName");
2239
- __publicField(this, "transportType");
2240
- __publicField(this, "serverUrl");
2241
- __publicField(this, "callbackUrl");
2242
- __publicField(this, "onRedirect");
2243
- __publicField(this, "clientId");
2244
- __publicField(this, "clientSecret");
2245
- __publicField(this, "headers");
2246
- /** OAuth Client Metadata */
2247
- __publicField(this, "clientName");
2248
- __publicField(this, "clientUri");
2249
- __publicField(this, "logoUri");
2250
- __publicField(this, "policyUri");
2372
+ __publicField(this, "config");
2251
2373
  __publicField(this, "createdAt");
2374
+ __publicField(this, "_serverInfo");
2375
+ __publicField(this, "_store");
2252
2376
  /** Event emitters for connection lifecycle */
2253
2377
  __publicField(this, "_onConnectionEvent", new Emitter());
2254
2378
  __publicField(this, "onConnectionEvent", this._onConnectionEvent.event);
@@ -2264,21 +2388,8 @@ var MCPClient = class {
2264
2388
  * the memory when the client is no longer needed.
2265
2389
  */
2266
2390
  __publicField(this, "cachedTools", null);
2267
- this.serverUrl = options.serverUrl;
2268
- this.serverName = options.serverName;
2269
- this.callbackUrl = options.callbackUrl;
2270
- this.onRedirect = options.onRedirect;
2271
- this.userId = options.userId;
2272
- this.serverId = options.serverId;
2273
- this.sessionId = options.sessionId;
2274
- this.transportType = options.transportType;
2275
- this.clientId = options.clientId;
2276
- this.clientSecret = options.clientSecret;
2277
- this.headers = options.headers;
2278
- this.clientName = options.clientName;
2279
- this.clientUri = options.clientUri;
2280
- this.logoUri = options.logoUri;
2281
- this.policyUri = options.policyUri;
2391
+ this.config = { ...options };
2392
+ this._store = options.sessionStore ?? sessions;
2282
2393
  this.client = new Client(
2283
2394
  {
2284
2395
  name: MCP_CLIENT_NAME,
@@ -2295,6 +2406,21 @@ var MCPClient = class {
2295
2406
  }
2296
2407
  );
2297
2408
  }
2409
+ /** Shared session-shaped data for ensureSession and saveSession */
2410
+ get session() {
2411
+ return {
2412
+ sessionId: this.config.sessionId,
2413
+ userId: this.config.userId,
2414
+ serverId: this.config.serverId,
2415
+ serverName: this.config.serverName,
2416
+ serverUrl: this.config.serverUrl,
2417
+ callbackUrl: this.config.callbackUrl,
2418
+ transportType: this.config.transportType || "streamable-http",
2419
+ headers: this.config.headers,
2420
+ createdAt: this.createdAt ?? Date.now(),
2421
+ updatedAt: Date.now()
2422
+ };
2423
+ }
2298
2424
  /**
2299
2425
  * Emit a connection state change event
2300
2426
  * @private
@@ -2302,13 +2428,13 @@ var MCPClient = class {
2302
2428
  emitStateChange(newState) {
2303
2429
  const previousState = this.currentState;
2304
2430
  this.currentState = newState;
2305
- if (!this.serverId) return;
2431
+ if (!this.config.serverId) return;
2306
2432
  this._onConnectionEvent.fire({
2307
2433
  type: "state_changed",
2308
- sessionId: this.sessionId,
2309
- serverId: this.serverId,
2310
- serverName: this.serverName || this.serverId,
2311
- serverUrl: this.serverUrl || "",
2434
+ sessionId: this.config.sessionId,
2435
+ serverId: this.config.serverId,
2436
+ serverName: this.config.serverName || this.config.serverId,
2437
+ serverUrl: this.config.serverUrl || "",
2312
2438
  createdAt: this.createdAt,
2313
2439
  state: newState,
2314
2440
  previousState,
@@ -2319,8 +2445,8 @@ var MCPClient = class {
2319
2445
  level: "info",
2320
2446
  message: `Connection state: ${previousState} \u2192 ${newState}`,
2321
2447
  displayMessage: `State changed to ${newState}`,
2322
- sessionId: this.sessionId,
2323
- serverId: this.serverId,
2448
+ sessionId: this.config.sessionId,
2449
+ serverId: this.config.serverId,
2324
2450
  payload: { previousState, newState },
2325
2451
  timestamp: Date.now(),
2326
2452
  id: nanoid()
@@ -2331,11 +2457,11 @@ var MCPClient = class {
2331
2457
  * @private
2332
2458
  */
2333
2459
  emitError(error, errorType = "unknown") {
2334
- if (!this.serverId) return;
2460
+ if (!this.config.serverId) return;
2335
2461
  this._onConnectionEvent.fire({
2336
2462
  type: "error",
2337
- sessionId: this.sessionId,
2338
- serverId: this.serverId,
2463
+ sessionId: this.config.sessionId,
2464
+ serverId: this.config.serverId,
2339
2465
  error,
2340
2466
  errorType,
2341
2467
  timestamp: Date.now()
@@ -2345,8 +2471,8 @@ var MCPClient = class {
2345
2471
  level: "error",
2346
2472
  message: error,
2347
2473
  displayMessage: error,
2348
- sessionId: this.sessionId,
2349
- serverId: this.serverId,
2474
+ sessionId: this.config.sessionId,
2475
+ serverId: this.config.serverId,
2350
2476
  payload: { errorType, error },
2351
2477
  timestamp: Date.now(),
2352
2478
  id: nanoid()
@@ -2357,11 +2483,11 @@ var MCPClient = class {
2357
2483
  * @private
2358
2484
  */
2359
2485
  emitProgress(message) {
2360
- if (!this.serverId) return;
2486
+ if (!this.config.serverId) return;
2361
2487
  this._onConnectionEvent.fire({
2362
2488
  type: "progress",
2363
- sessionId: this.sessionId,
2364
- serverId: this.serverId,
2489
+ sessionId: this.config.sessionId,
2490
+ serverId: this.config.serverId,
2365
2491
  message,
2366
2492
  timestamp: Date.now()
2367
2493
  });
@@ -2379,14 +2505,14 @@ var MCPClient = class {
2379
2505
  * @private
2380
2506
  */
2381
2507
  getTransport(type) {
2382
- if (!this.serverUrl) {
2508
+ if (!this.config.serverUrl) {
2383
2509
  throw new Error("Server URL is required to create transport");
2384
2510
  }
2385
- const baseUrl = new URL(this.serverUrl);
2386
- const hasAuthorizationHeader = Object.keys(this.headers || {}).some((key) => key.toLowerCase() === "authorization");
2511
+ const baseUrl = new URL(this.config.serverUrl);
2512
+ const hasAuthorizationHeader = Object.keys(this.config.headers || {}).some((key) => key.toLowerCase() === "authorization");
2387
2513
  const transportOptions = {
2388
2514
  ...!hasAuthorizationHeader && { authProvider: this.oauthProvider },
2389
- ...this.headers && { requestInit: { headers: this.headers } },
2515
+ ...this.config.headers && { requestInit: { headers: this.config.headers } },
2390
2516
  /**
2391
2517
  * Custom fetch implementation to handle connection timeouts.
2392
2518
  * Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
@@ -2424,78 +2550,74 @@ var MCPClient = class {
2424
2550
  * @private
2425
2551
  */
2426
2552
  async ensureSession() {
2427
- if (this.oauthProvider) {
2428
- return;
2429
- }
2553
+ if (this.oauthProvider) return;
2430
2554
  this.emitStateChange("INITIALIZING");
2431
2555
  this.emitProgress("Loading session configuration...");
2432
- let existingSession = null;
2433
- if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
2434
- existingSession = await sessions.get(this.userId, this.sessionId);
2435
- if (!existingSession) {
2436
- throw new Error(`Session not found: ${this.sessionId}`);
2556
+ if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
2557
+ const existingSession2 = await this._store.get(this.config.userId, this.config.sessionId);
2558
+ if (!existingSession2) {
2559
+ throw new Error(`Session not found: ${this.config.sessionId}`);
2437
2560
  }
2438
- this.serverUrl = this.serverUrl || existingSession.serverUrl;
2439
- this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
2440
- this.serverName = this.serverName || existingSession.serverName;
2441
- this.serverId = this.serverId || existingSession.serverId || "unknown";
2442
- this.headers = this.headers || existingSession.headers;
2443
- this.createdAt = existingSession.createdAt;
2444
- }
2445
- if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
2561
+ this.config.serverUrl = this.config.serverUrl || existingSession2.serverUrl;
2562
+ this.config.callbackUrl = this.config.callbackUrl || existingSession2.callbackUrl;
2563
+ this.config.serverName = this.config.serverName || existingSession2.serverName;
2564
+ this.config.serverId = this.config.serverId || existingSession2.serverId || "unknown";
2565
+ this.config.headers = this.config.headers || existingSession2.headers;
2566
+ this.createdAt = existingSession2.createdAt;
2567
+ }
2568
+ if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
2446
2569
  throw new Error("Missing required connection metadata");
2447
2570
  }
2448
- if (!this.oauthProvider) {
2449
- if (!this.serverId) {
2450
- throw new Error("serverId required for OAuth provider initialization");
2451
- }
2452
- this.oauthProvider = new StorageOAuthClientProvider({
2453
- userId: this.userId,
2454
- serverId: this.serverId,
2455
- sessionId: this.sessionId,
2456
- redirectUrl: this.callbackUrl,
2457
- clientName: this.clientName,
2458
- clientUri: this.clientUri,
2459
- logoUri: this.logoUri,
2460
- policyUri: this.policyUri,
2461
- clientId: this.clientId,
2462
- clientSecret: this.clientSecret,
2463
- onRedirect: (redirectUrl) => {
2464
- if (this.onRedirect) {
2465
- this.onRedirect(redirectUrl);
2466
- }
2571
+ this.oauthProvider = new StorageOAuthClientProvider({
2572
+ userId: this.config.userId,
2573
+ serverId: this.config.serverId,
2574
+ sessionId: this.config.sessionId,
2575
+ redirectUrl: this.config.callbackUrl,
2576
+ clientName: this.config.clientName,
2577
+ clientUri: this.config.clientUri,
2578
+ logoUri: this.config.logoUri,
2579
+ policyUri: this.config.policyUri,
2580
+ clientId: this.config.clientId,
2581
+ clientSecret: this.config.clientSecret,
2582
+ cachedTokens: this.config.cachedCredentials?.tokens,
2583
+ sessionStore: this._store,
2584
+ onRedirect: (redirectUrl) => {
2585
+ if (this.config.serverId) {
2586
+ this._onConnectionEvent.fire({
2587
+ type: "auth_required",
2588
+ sessionId: this.config.sessionId,
2589
+ serverId: this.config.serverId,
2590
+ authUrl: redirectUrl,
2591
+ timestamp: Date.now()
2592
+ });
2467
2593
  }
2468
- });
2469
- }
2470
- if (existingSession === null) {
2471
- existingSession = await sessions.get(this.userId, this.sessionId);
2472
- }
2473
- if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
2594
+ if (this.config.onRedirect) {
2595
+ this.config.onRedirect(redirectUrl);
2596
+ }
2597
+ }
2598
+ });
2599
+ const existingSession = this.config.hasSession ? {} : await this._store.get(this.config.userId, this.config.sessionId);
2600
+ if (!existingSession) {
2474
2601
  this.createdAt = Date.now();
2475
2602
  const updatedAt = this.createdAt;
2476
2603
  this._onObservabilityEvent.fire({
2477
2604
  type: "mcp:client:session_created",
2478
2605
  level: "info",
2479
- message: `Creating pending session ${this.sessionId} for connection setup`,
2480
- sessionId: this.sessionId,
2481
- serverId: this.serverId,
2606
+ message: `Creating pending session ${this.config.sessionId} for connection setup`,
2607
+ sessionId: this.config.sessionId,
2608
+ serverId: this.config.serverId,
2482
2609
  timestamp: Date.now(),
2483
2610
  id: nanoid()
2484
2611
  });
2485
- await sessions.create({
2486
- sessionId: this.sessionId,
2487
- userId: this.userId,
2488
- serverId: this.serverId,
2489
- serverName: this.serverName,
2490
- serverUrl: this.serverUrl,
2491
- callbackUrl: this.callbackUrl,
2492
- transportType: this.transportType || "streamable-http",
2493
- headers: this.headers,
2494
- createdAt: this.createdAt,
2612
+ await this._store.create({
2613
+ ...this.session,
2495
2614
  updatedAt,
2496
2615
  status: "pending"
2497
2616
  });
2498
2617
  }
2618
+ if (!existingSession && this.oauthProvider instanceof StorageOAuthClientProvider) {
2619
+ await this.oauthProvider.initializeCredentials();
2620
+ }
2499
2621
  }
2500
2622
  /**
2501
2623
  * Saves current session state to the session store
@@ -2503,34 +2625,14 @@ var MCPClient = class {
2503
2625
  * @param status - Session lifecycle status used by storage cleanup
2504
2626
  * @private
2505
2627
  */
2506
- async saveSession(status = "active", existingSession) {
2507
- if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
2628
+ async saveSession(status = "active") {
2629
+ if (!this.config.sessionId || !this.config.serverId || !this.config.serverUrl || !this.config.callbackUrl) {
2508
2630
  return;
2509
2631
  }
2510
- const sessionData = {
2511
- sessionId: this.sessionId,
2512
- userId: this.userId,
2513
- serverId: this.serverId,
2514
- serverName: this.serverName,
2515
- serverUrl: this.serverUrl,
2516
- callbackUrl: this.callbackUrl,
2517
- transportType: this.transportType || "streamable-http",
2518
- headers: this.headers,
2519
- createdAt: this.createdAt || Date.now(),
2520
- updatedAt: Date.now(),
2632
+ await this._store.update(this.config.userId, this.config.sessionId, {
2633
+ ...this.session,
2521
2634
  status
2522
- };
2523
- if (status === "active") {
2524
- sessionData.authUrl = null;
2525
- }
2526
- if (existingSession === void 0) {
2527
- existingSession = await sessions.get(this.userId, this.sessionId);
2528
- }
2529
- if (existingSession) {
2530
- await sessions.update(this.userId, this.sessionId, sessionData);
2531
- } else {
2532
- await sessions.create(sessionData);
2533
- }
2635
+ });
2534
2636
  }
2535
2637
  /**
2536
2638
  * Removes transient setup/auth sessions without masking the original error.
@@ -2538,7 +2640,7 @@ var MCPClient = class {
2538
2640
  */
2539
2641
  async deleteTransientSession() {
2540
2642
  try {
2541
- await sessions.delete(this.userId, this.sessionId);
2643
+ await this._store.delete(this.config.userId, this.config.sessionId);
2542
2644
  } catch {
2543
2645
  }
2544
2646
  }
@@ -2548,7 +2650,7 @@ var MCPClient = class {
2548
2650
  * @private
2549
2651
  */
2550
2652
  async tryConnect() {
2551
- const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
2653
+ const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
2552
2654
  let lastError;
2553
2655
  for (const currentType of transportsToTry) {
2554
2656
  const isLastAttempt = currentType === transportsToTry[transportsToTry.length - 1];
@@ -2556,6 +2658,7 @@ var MCPClient = class {
2556
2658
  const transport = this.getTransport(currentType);
2557
2659
  this.transport = transport;
2558
2660
  await this.client.connect(transport);
2661
+ this._serverInfo = this.client.getServerVersion();
2559
2662
  return { transportType: currentType };
2560
2663
  } catch (error) {
2561
2664
  lastError = error;
@@ -2571,8 +2674,8 @@ var MCPClient = class {
2571
2674
  this._onObservabilityEvent.fire({
2572
2675
  level: "warn",
2573
2676
  message: `Transport ${currentType} failed, falling back`,
2574
- sessionId: this.sessionId,
2575
- serverId: this.serverId,
2677
+ sessionId: this.config.sessionId,
2678
+ serverId: this.config.serverId,
2576
2679
  metadata: {
2577
2680
  failedTransport: currentType,
2578
2681
  error: errorMessage
@@ -2615,15 +2718,15 @@ var MCPClient = class {
2615
2718
  try {
2616
2719
  this.emitStateChange("CONNECTING");
2617
2720
  const { transportType } = await this.tryConnect();
2618
- this.transportType = transportType;
2721
+ this.config.transportType = transportType;
2619
2722
  this.emitStateChange("CONNECTED");
2620
2723
  this.emitProgress("Connected successfully");
2621
2724
  this._onObservabilityEvent.fire({
2622
2725
  type: "mcp:client:session_saved",
2623
2726
  level: "info",
2624
- message: `Saving active session ${this.sessionId} (connect success)`,
2625
- sessionId: this.sessionId,
2626
- serverId: this.serverId,
2727
+ message: `Saving active session ${this.config.sessionId} (connect success)`,
2728
+ sessionId: this.config.sessionId,
2729
+ serverId: this.config.serverId,
2627
2730
  timestamp: Date.now(),
2628
2731
  id: nanoid()
2629
2732
  });
@@ -2646,23 +2749,23 @@ var MCPClient = class {
2646
2749
  this._onObservabilityEvent.fire({
2647
2750
  type: "mcp:client:session_saved",
2648
2751
  level: "info",
2649
- message: `Saving pending OAuth session ${this.sessionId}`,
2650
- sessionId: this.sessionId,
2651
- serverId: this.serverId,
2752
+ message: `Saving pending OAuth session ${this.config.sessionId}`,
2753
+ sessionId: this.config.sessionId,
2754
+ serverId: this.config.serverId,
2652
2755
  timestamp: Date.now(),
2653
2756
  id: nanoid()
2654
2757
  });
2655
2758
  await this.saveSession("pending");
2656
- if (this.serverId) {
2759
+ if (this.config.serverId) {
2657
2760
  this._onConnectionEvent.fire({
2658
2761
  type: "auth_required",
2659
- sessionId: this.sessionId,
2660
- serverId: this.serverId,
2762
+ sessionId: this.config.sessionId,
2763
+ serverId: this.config.serverId,
2661
2764
  authUrl,
2662
2765
  timestamp: Date.now()
2663
2766
  });
2664
- if (authUrl && this.onRedirect) {
2665
- this.onRedirect(authUrl);
2767
+ if (authUrl && this.config.onRedirect) {
2768
+ this.config.onRedirect(authUrl);
2666
2769
  }
2667
2770
  }
2668
2771
  throw new UnauthorizedError("OAuth authorization required");
@@ -2671,9 +2774,9 @@ var MCPClient = class {
2671
2774
  this.emitError(errorMessage, "connection");
2672
2775
  this.emitStateChange("FAILED");
2673
2776
  try {
2674
- const existingSession = await sessions.get(this.userId, this.sessionId);
2777
+ const existingSession = await this._store.get(this.config.userId, this.config.sessionId);
2675
2778
  if (!existingSession || existingSession.status !== "active") {
2676
- await sessions.delete(this.userId, this.sessionId);
2779
+ await this._store.delete(this.config.userId, this.config.sessionId);
2677
2780
  }
2678
2781
  } catch {
2679
2782
  }
@@ -2698,16 +2801,16 @@ var MCPClient = class {
2698
2801
  throw new Error(error);
2699
2802
  }
2700
2803
  if (state) {
2701
- const stateCheck = await this.oauthProvider.checkState(state);
2702
- if (!stateCheck.valid) {
2703
- const error = stateCheck.error || "Invalid OAuth state";
2704
- this.emitError(error, "auth");
2804
+ try {
2805
+ await this.oauthProvider.consumeState(state);
2806
+ } catch (error) {
2807
+ const msg = error instanceof Error ? error.message : "Invalid OAuth state";
2808
+ this.emitError(msg, "auth");
2705
2809
  this.emitStateChange("FAILED");
2706
- throw new Error(error);
2810
+ throw error;
2707
2811
  }
2708
- await this.oauthProvider.consumeState(state);
2709
2812
  }
2710
- const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
2813
+ const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
2711
2814
  let lastError;
2712
2815
  let tokensExchanged = false;
2713
2816
  let authenticatedStateEmitted = false;
@@ -2734,14 +2837,15 @@ var MCPClient = class {
2734
2837
  }
2735
2838
  }
2736
2839
  await this.client.connect(this.transport);
2737
- this.transportType = currentType;
2840
+ this._serverInfo = this.client.getServerVersion();
2841
+ this.config.transportType = currentType;
2738
2842
  this.emitStateChange("CONNECTED");
2739
2843
  this._onObservabilityEvent.fire({
2740
2844
  type: "mcp:client:session_saved",
2741
2845
  level: "info",
2742
- message: `Saving active session ${this.sessionId} (OAuth complete)`,
2743
- sessionId: this.sessionId,
2744
- serverId: this.serverId,
2846
+ message: `Saving active session ${this.config.sessionId} (OAuth complete)`,
2847
+ sessionId: this.config.sessionId,
2848
+ serverId: this.config.serverId,
2745
2849
  timestamp: Date.now(),
2746
2850
  id: nanoid()
2747
2851
  });
@@ -2814,11 +2918,11 @@ var MCPClient = class {
2814
2918
  this.emitStateChange("DISCOVERING");
2815
2919
  try {
2816
2920
  const result = await this.fetchTools();
2817
- if (this.serverId) {
2921
+ if (this.config.serverId) {
2818
2922
  this._onConnectionEvent.fire({
2819
2923
  type: "tools_discovered",
2820
- sessionId: this.sessionId,
2821
- serverId: this.serverId,
2924
+ sessionId: this.config.sessionId,
2925
+ serverId: this.config.serverId,
2822
2926
  toolCount: result.tools.length,
2823
2927
  tools: result.tools,
2824
2928
  timestamp: Date.now()
@@ -2858,8 +2962,8 @@ var MCPClient = class {
2858
2962
  level: "info",
2859
2963
  message: `Tool ${toolName} called successfully`,
2860
2964
  displayMessage: `Called tool ${toolName}`,
2861
- sessionId: this.sessionId,
2862
- serverId: this.serverId,
2965
+ sessionId: this.config.sessionId,
2966
+ serverId: this.config.serverId,
2863
2967
  payload: {
2864
2968
  toolName,
2865
2969
  args: toolArgs
@@ -2875,8 +2979,8 @@ var MCPClient = class {
2875
2979
  level: "error",
2876
2980
  message: errorMessage,
2877
2981
  displayMessage: `Failed to call tool ${toolName}`,
2878
- sessionId: this.sessionId,
2879
- serverId: this.serverId,
2982
+ sessionId: this.config.sessionId,
2983
+ serverId: this.config.serverId,
2880
2984
  payload: {
2881
2985
  errorType: "tool_execution",
2882
2986
  error: errorMessage,
@@ -3024,8 +3128,8 @@ var MCPClient = class {
3024
3128
  type: "mcp:client:error",
3025
3129
  level: "warn",
3026
3130
  message: "Initialization failed during clearSession",
3027
- sessionId: this.sessionId,
3028
- serverId: this.serverId,
3131
+ sessionId: this.config.sessionId,
3132
+ serverId: this.config.serverId,
3029
3133
  payload: { error: String(error) },
3030
3134
  timestamp: Date.now(),
3031
3135
  id: nanoid()
@@ -3034,7 +3138,7 @@ var MCPClient = class {
3034
3138
  if (this.oauthProvider) {
3035
3139
  await this.oauthProvider.invalidateCredentials("all");
3036
3140
  }
3037
- await sessions.delete(this.userId, this.sessionId);
3141
+ await this._store.delete(this.config.userId, this.config.sessionId);
3038
3142
  await this.disconnect();
3039
3143
  }
3040
3144
  /**
@@ -3068,19 +3172,19 @@ var MCPClient = class {
3068
3172
  }
3069
3173
  this.oauthProvider = null;
3070
3174
  this.transport = null;
3071
- if (this.serverId) {
3175
+ if (this.config.serverId) {
3072
3176
  this._onConnectionEvent.fire({
3073
3177
  type: "disconnected",
3074
- sessionId: this.sessionId,
3075
- serverId: this.serverId,
3178
+ sessionId: this.config.sessionId,
3179
+ serverId: this.config.serverId,
3076
3180
  timestamp: Date.now()
3077
3181
  });
3078
3182
  this._onObservabilityEvent.fire({
3079
3183
  type: "mcp:client:disconnect",
3080
3184
  level: "info",
3081
- message: `Disconnected from ${this.serverId}`,
3082
- sessionId: this.sessionId,
3083
- serverId: this.serverId,
3185
+ message: `Disconnected from ${this.config.serverId}`,
3186
+ sessionId: this.config.sessionId,
3187
+ serverId: this.config.serverId,
3084
3188
  payload: {},
3085
3189
  timestamp: Date.now(),
3086
3190
  id: nanoid()
@@ -3105,42 +3209,52 @@ var MCPClient = class {
3105
3209
  * @returns Server URL or empty string if not set
3106
3210
  */
3107
3211
  getServerUrl() {
3108
- return this.serverUrl || "";
3212
+ return this.config.serverUrl || "";
3109
3213
  }
3110
3214
  /**
3111
3215
  * Gets the OAuth callback URL
3112
3216
  * @returns Callback URL or empty string if not set
3113
3217
  */
3114
3218
  getCallbackUrl() {
3115
- return this.callbackUrl || "";
3219
+ return this.config.callbackUrl || "";
3116
3220
  }
3117
3221
  /**
3118
3222
  * Gets the transport type being used
3119
3223
  * @returns Transport type (defaults to 'streamable-http')
3120
3224
  */
3121
3225
  getTransportType() {
3122
- return this.transportType || "streamable-http";
3226
+ return this.config.transportType || "streamable-http";
3227
+ }
3228
+ /**
3229
+ * Gets the full server metadata from the MCP initialize response.
3230
+ * Includes name, version, icons, title, description, and website URL.
3231
+ * Returns undefined if the client hasn't connected yet.
3232
+ */
3233
+ getServerInfo() {
3234
+ return this._serverInfo;
3123
3235
  }
3124
3236
  /**
3125
- * Gets the human-readable server name
3237
+ * Gets the human-readable server name.
3238
+ * Prefers the server's reported title/name from the initialize response,
3239
+ * falling back to the name provided at construction or session metadata.
3126
3240
  * @returns Server name or undefined
3127
3241
  */
3128
3242
  getServerName() {
3129
- return this.serverName;
3243
+ return this.config.serverName;
3130
3244
  }
3131
3245
  /**
3132
3246
  * Gets the server ID
3133
3247
  * @returns Server ID or undefined
3134
3248
  */
3135
3249
  getServerId() {
3136
- return this.serverId;
3250
+ return this.config.serverId;
3137
3251
  }
3138
3252
  /**
3139
3253
  * Gets the session ID
3140
3254
  * @returns Session ID
3141
3255
  */
3142
3256
  getSessionId() {
3143
- return this.sessionId;
3257
+ return this.config.sessionId;
3144
3258
  }
3145
3259
  };
3146
3260
 
@@ -3163,6 +3277,14 @@ var ToolPolicyGateway = class {
3163
3277
  getServerId() {
3164
3278
  return this.client.getServerId?.();
3165
3279
  }
3280
+ /**
3281
+ * Returns the full server metadata from the underlying client, if available.
3282
+ * Includes name, version, icons, title, description, and website URL.
3283
+ * Available only after the client has connected and completed initialization.
3284
+ */
3285
+ getServerInfo() {
3286
+ return this.client.getServerInfo?.();
3287
+ }
3166
3288
  /**
3167
3289
  * Returns the human-readable server name from the underlying client, if available.
3168
3290
  */
@@ -3291,6 +3413,7 @@ var MultiSessionClient = class {
3291
3413
  __publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
3292
3414
  __publicField(this, "userId");
3293
3415
  __publicField(this, "options");
3416
+ __publicField(this, "_store");
3294
3417
  this.userId = userId;
3295
3418
  this.options = {
3296
3419
  timeout: DEFAULT_TIMEOUT_MS,
@@ -3298,6 +3421,7 @@ var MultiSessionClient = class {
3298
3421
  retryDelay: DEFAULT_RETRY_DELAY_MS,
3299
3422
  ...options
3300
3423
  };
3424
+ this._store = options.onObservabilityEvent ? withDbObservability(options.sessionStore ?? sessions, options.onObservabilityEvent) : options.sessionStore ?? sessions;
3301
3425
  }
3302
3426
  // -----------------------------------------------------------------------
3303
3427
  // Public API
@@ -3381,9 +3505,9 @@ var MultiSessionClient = class {
3381
3505
  * to querying the storage backend via `sessions.list(userId)`.
3382
3506
  */
3383
3507
  async fetchActiveSessions() {
3384
- const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await sessions.list(this.userId);
3508
+ const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await this._store.list(this.userId);
3385
3509
  return sessionList.filter(
3386
- (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
3510
+ (s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active" && s.enabled !== false
3387
3511
  );
3388
3512
  }
3389
3513
  /**
@@ -3456,8 +3580,13 @@ var MultiSessionClient = class {
3456
3580
  callbackUrl: session.callbackUrl,
3457
3581
  serverName: session.serverName,
3458
3582
  transportType: session.transportType,
3459
- headers: session.headers
3583
+ headers: session.headers,
3584
+ hasSession: true,
3585
+ sessionStore: this._store
3460
3586
  });
3587
+ if (this.options.onObservabilityEvent) {
3588
+ client.onObservabilityEvent(this.options.onObservabilityEvent);
3589
+ }
3461
3590
  const timeoutMs = this.options.timeout;
3462
3591
  let timeoutTimer;
3463
3592
  const timeoutPromise = new Promise((_, reject) => {
@@ -3512,130 +3641,188 @@ function isConnectionEvent(event) {
3512
3641
  }
3513
3642
 
3514
3643
  // src/server/handlers/sse-handler.ts
3515
- var DEFAULT_HEARTBEAT_INTERVAL = 3e4;
3644
+ var DEFAULT_HEARTBEAT_MS = 3e4;
3516
3645
  function normalizeHeaders(headers) {
3517
3646
  if (!headers || typeof headers !== "object") return void 0;
3518
- const entries = Object.entries(headers).map(([key, value]) => [key.trim(), String(value).trim()]).filter(([key, value]) => key.length > 0 && value.length > 0);
3647
+ const entries = Object.entries(headers).map(([k, v]) => [k.trim(), String(v).trim()]).filter(([k, v]) => k.length > 0 && v.length > 0);
3519
3648
  return entries.length > 0 ? Object.fromEntries(entries) : void 0;
3520
3649
  }
3650
+ function validatingStateEvent(sessionId, session) {
3651
+ return {
3652
+ type: "state_changed",
3653
+ sessionId,
3654
+ serverId: session.serverId ?? "unknown",
3655
+ serverName: session.serverName ?? "Unknown",
3656
+ serverUrl: session.serverUrl,
3657
+ state: "VALIDATING",
3658
+ previousState: "DISCONNECTED",
3659
+ timestamp: Date.now()
3660
+ };
3661
+ }
3662
+ function connectionErrorEvent(sessionId, serverId, error, errorType) {
3663
+ return {
3664
+ type: "error",
3665
+ sessionId,
3666
+ serverId: serverId ?? "unknown",
3667
+ error: error instanceof Error ? error.message : "Connection failed",
3668
+ errorType,
3669
+ timestamp: Date.now()
3670
+ };
3671
+ }
3521
3672
  var SSEConnectionManager = class {
3673
+ // -----------------------------------------------------------------------
3674
+ // Constructor
3675
+ // -----------------------------------------------------------------------
3676
+ /**
3677
+ * @param options - Handler configuration (userId, auth, metadata, heartbeat, observability).
3678
+ * @param sendEvent - Callback that writes a typed event onto the SSE stream.
3679
+ */
3522
3680
  constructor(options, sendEvent) {
3523
3681
  this.options = options;
3524
3682
  this.sendEvent = sendEvent;
3683
+ // -----------------------------------------------------------------------
3684
+ // State
3685
+ // -----------------------------------------------------------------------
3525
3686
  __publicField(this, "userId");
3687
+ /** Active MCP transports keyed by sessionId. */
3526
3688
  __publicField(this, "clients", /* @__PURE__ */ new Map());
3689
+ /** Instrumented session store — always wraps `sessions` with DB observability. */
3690
+ __publicField(this, "observedStore");
3527
3691
  __publicField(this, "heartbeatTimer");
3528
- __publicField(this, "isActive", true);
3692
+ __publicField(this, "active", true);
3529
3693
  this.userId = options.userId;
3694
+ this.observedStore = withDbObservability(sessions, (event) => this.sendEvent(event));
3530
3695
  this.startHeartbeat();
3531
3696
  }
3697
+ // -----------------------------------------------------------------------
3698
+ // Public API
3699
+ // -----------------------------------------------------------------------
3532
3700
  /**
3533
- * Get resolved client metadata (dynamic > static > defaults)
3534
- */
3535
- async getResolvedClientMetadata(request) {
3536
- let metadata = {};
3537
- if (this.options.clientDefaults) {
3538
- metadata = { ...this.options.clientDefaults };
3539
- }
3540
- if (this.options.getClientMetadata) {
3541
- const dynamicMetadata = await this.options.getClientMetadata(request);
3542
- metadata = { ...metadata, ...dynamicMetadata };
3543
- }
3544
- return metadata;
3545
- }
3546
- /**
3547
- * Start heartbeat to keep connection alive
3548
- */
3549
- startHeartbeat() {
3550
- const interval = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL;
3551
- this.heartbeatTimer = setInterval(() => {
3552
- if (this.isActive) {
3553
- this.sendEvent({
3554
- level: "debug",
3555
- message: "heartbeat",
3556
- timestamp: Date.now()
3557
- });
3558
- }
3559
- }, interval);
3560
- }
3561
- /**
3562
- * Handle incoming RPC requests
3563
- * Returns the RPC response directly for immediate HTTP response (bypassing SSE latency)
3701
+ * Dispatches an incoming RPC request, emits timing observability,
3702
+ * and returns the response.
3703
+ *
3704
+ * Emits `rpc:start` before dispatch and `rpc:end` on completion
3705
+ * (success or error), each carrying the method name, sessionId, and
3706
+ * duration. All events flow through the unified `onObservability`
3707
+ * and the SSE stream.
3708
+ *
3709
+ * @param request - The deserialized RPC envelope.
3710
+ * @returns The RPC response (success or error).
3564
3711
  */
3565
3712
  async handleRequest(request) {
3713
+ const method = request.method;
3714
+ const sessionId = request.params?.sessionId;
3715
+ const t0 = performance.now();
3716
+ this.sendEvent({
3717
+ type: "rpc:start",
3718
+ level: "debug",
3719
+ message: method,
3720
+ sessionId,
3721
+ timestamp: Date.now()
3722
+ });
3566
3723
  try {
3567
- let result;
3568
- switch (request.method) {
3569
- case "listSessions":
3570
- result = await this.listSessions();
3571
- break;
3572
- case "connect":
3573
- result = await this.connect(request.params);
3574
- break;
3575
- case "reconnect":
3576
- result = await this.reconnect(request.params);
3577
- break;
3578
- case "disconnect":
3579
- result = await this.disconnect(request.params);
3580
- break;
3581
- case "listTools":
3582
- result = await this.listTools(request.params);
3583
- break;
3584
- case "setToolPolicy":
3585
- result = await this.setToolPolicy(request.params);
3586
- break;
3587
- case "getToolPolicy":
3588
- result = await this.getToolPolicy(request.params);
3589
- break;
3590
- case "callTool":
3591
- result = await this.callTool(request.params);
3592
- break;
3593
- case "getSession":
3594
- result = await this.getSession(request.params);
3595
- break;
3596
- case "finishAuth":
3597
- result = await this.finishAuth(request.params);
3598
- break;
3599
- case "listPrompts":
3600
- result = await this.listPrompts(request.params);
3601
- break;
3602
- case "getPrompt":
3603
- result = await this.getPrompt(request.params);
3604
- break;
3605
- case "listResources":
3606
- result = await this.listResources(request.params);
3607
- break;
3608
- case "readResource":
3609
- result = await this.readResource(request.params);
3610
- break;
3611
- default:
3612
- throw new Error(`Unknown method: ${request.method}`);
3613
- }
3614
- const response = {
3615
- id: request.id,
3616
- result
3617
- };
3724
+ const result = await this.dispatchImpl(request);
3725
+ this.sendEvent({
3726
+ type: "rpc:end",
3727
+ level: "debug",
3728
+ message: method,
3729
+ sessionId,
3730
+ payload: { durationMs: performance.now() - t0 },
3731
+ timestamp: Date.now()
3732
+ });
3733
+ const response = { id: request.id, result };
3618
3734
  this.sendEvent(response);
3619
3735
  return response;
3620
3736
  } catch (error) {
3621
- const errorResponse = {
3737
+ this.sendEvent({
3738
+ type: "rpc:end",
3739
+ level: "error",
3740
+ message: method,
3741
+ sessionId,
3742
+ payload: { durationMs: performance.now() - t0, error: String(error) },
3743
+ timestamp: Date.now()
3744
+ });
3745
+ const response = {
3622
3746
  id: request.id,
3623
3747
  error: {
3624
3748
  code: RpcErrorCodes.EXECUTION_ERROR,
3625
3749
  message: error instanceof Error ? error.message : "Unknown error"
3626
3750
  }
3627
3751
  };
3628
- this.sendEvent(errorResponse);
3629
- return errorResponse;
3752
+ this.sendEvent(response);
3753
+ return response;
3630
3754
  }
3631
3755
  }
3632
3756
  /**
3633
- * Get all sessions for the current userId
3757
+ * Tears down all active MCP transports and stops the heartbeat timer.
3758
+ *
3759
+ * Disconnects are issued in parallel across all sessions. After calling
3760
+ * this method the manager instance should be discarded.
3761
+ */
3762
+ async dispose() {
3763
+ this.active = false;
3764
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
3765
+ await Promise.all(
3766
+ Array.from(this.clients.values()).map((c) => c.disconnect())
3767
+ );
3768
+ this.clients.clear();
3769
+ }
3770
+ // -----------------------------------------------------------------------
3771
+ // RPC Dispatch (raw — called by handleRequest which adds timing)
3772
+ // -----------------------------------------------------------------------
3773
+ /**
3774
+ * Routes an RPC method name to the appropriate private handler.
3775
+ *
3776
+ * @throws {Error} When the method name is unrecognized.
3777
+ */
3778
+ async dispatchImpl(request) {
3779
+ switch (request.method) {
3780
+ case "listSessions":
3781
+ return this.listSessions();
3782
+ case "connect":
3783
+ return this.connect(request.params);
3784
+ case "reconnect":
3785
+ return this.reconnect(request.params);
3786
+ case "disconnect":
3787
+ return this.disconnect(request.params);
3788
+ case "listTools":
3789
+ return this.listTools(request.params);
3790
+ case "setToolPolicy":
3791
+ return this.setToolPolicy(request.params);
3792
+ case "getToolPolicy":
3793
+ return this.getToolPolicy(request.params);
3794
+ case "updateSession":
3795
+ return this.updateSession(request.params);
3796
+ case "callTool":
3797
+ return this.callTool(request.params);
3798
+ case "getSession":
3799
+ return this.getSession(request.params);
3800
+ case "finishAuth":
3801
+ return this.finishAuth(request.params);
3802
+ case "listPrompts":
3803
+ return this.listPrompts(request.params);
3804
+ case "getPrompt":
3805
+ return this.getPrompt(request.params);
3806
+ case "listResources":
3807
+ return this.listResources(request.params);
3808
+ case "readResource":
3809
+ return this.readResource(request.params);
3810
+ default:
3811
+ throw new Error(`Unknown RPC method: ${request.method}`);
3812
+ }
3813
+ }
3814
+ // -----------------------------------------------------------------------
3815
+ // Session Query
3816
+ // -----------------------------------------------------------------------
3817
+ /**
3818
+ * Lists all sessions owned by the current user.
3819
+ *
3820
+ * Returns a lightweight view — session metadata only, no credential fields.
3634
3821
  */
3635
3822
  async listSessions() {
3636
- const sessionList = await sessions.list(this.userId);
3823
+ const all = await sessions.list(this.userId);
3637
3824
  return {
3638
- sessions: sessionList.map((s) => ({
3825
+ sessions: all.map((s) => ({
3639
3826
  sessionId: s.sessionId,
3640
3827
  serverId: s.serverId,
3641
3828
  serverName: s.serverName,
@@ -3644,215 +3831,203 @@ var SSEConnectionManager = class {
3644
3831
  createdAt: s.createdAt,
3645
3832
  updatedAt: s.updatedAt ?? s.createdAt,
3646
3833
  status: s.status ?? "pending",
3647
- toolPolicy: s.toolPolicy
3834
+ toolPolicy: s.toolPolicy,
3835
+ enabled: s.enabled ?? true
3648
3836
  }))
3649
3837
  };
3650
3838
  }
3651
3839
  /**
3652
- * Connect to an MCP server
3840
+ * Restores and validates a previously persisted session.
3841
+ *
3842
+ * Loads the full session row (including credentials) from storage, creates
3843
+ * a new MCP transport, connects to the remote server, and emits the
3844
+ * discovered (policy-filtered) tool list via SSE.
3653
3845
  */
3654
- async connect(params) {
3655
- const { serverName, serverUrl, callbackUrl, transportType, clientId, clientSecret } = params;
3656
- const headers = normalizeHeaders(params.headers);
3657
- const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : generateServerId();
3658
- const existingSessions = await sessions.list(this.userId);
3659
- const duplicate = existingSessions.find(
3660
- (s) => s.serverId === serverId || s.serverUrl === serverUrl
3661
- );
3662
- if (duplicate) {
3663
- if (duplicate.status === "pending") {
3664
- await this.getSession({ sessionId: duplicate.sessionId });
3665
- return {
3666
- sessionId: duplicate.sessionId,
3667
- success: true
3668
- };
3669
- }
3670
- throw new Error(`Connection already exists for server: ${duplicate.serverUrl || duplicate.serverId} (${duplicate.serverName})`);
3671
- }
3672
- const sessionId = await sessions.generateSessionId();
3846
+ async getSession(params) {
3847
+ const session = await this.requireSession(params.sessionId);
3848
+ this.sendEvent(validatingStateEvent(params.sessionId, session));
3673
3849
  try {
3674
- const clientMetadata = await this.getResolvedClientMetadata();
3675
- const client = new MCPClient({
3676
- userId: this.userId,
3677
- sessionId,
3678
- serverId,
3679
- serverName,
3680
- serverUrl,
3681
- callbackUrl,
3682
- transportType,
3683
- headers,
3684
- clientId,
3685
- clientSecret,
3686
- ...clientMetadata
3687
- // Spread client metadata (clientName, clientUri, logoUri, policyUri)
3688
- });
3689
- this.clients.set(sessionId, client);
3690
- client.onConnectionEvent((event) => {
3691
- this.emitConnectionEvent(event);
3692
- });
3693
- client.onObservabilityEvent((event) => {
3694
- this.sendEvent(event);
3695
- });
3850
+ const client = this.restoreClient(session);
3851
+ this.attachClientEvents(client);
3696
3852
  await client.connect();
3697
- await this.listPolicyFilteredTools(sessionId);
3698
- return {
3699
- sessionId,
3700
- success: true
3701
- };
3853
+ this.clients.set(params.sessionId, client);
3854
+ const { result } = await this.listPolicyFilteredTools(params.sessionId);
3855
+ return { success: true, toolCount: result.tools.length };
3702
3856
  } catch (error) {
3703
- if (error instanceof UnauthorizedError) {
3704
- this.clients.delete(sessionId);
3705
- return {
3706
- sessionId,
3707
- success: true
3708
- };
3709
- }
3710
- this.emitConnectionEvent({
3711
- type: "error",
3712
- sessionId,
3713
- serverId,
3714
- error: error instanceof Error ? error.message : "Connection failed",
3715
- errorType: "connection",
3716
- timestamp: Date.now()
3717
- });
3718
- this.clients.delete(sessionId);
3857
+ this.sendEvent(connectionErrorEvent(params.sessionId, session.serverId, error, "validation"));
3719
3858
  throw error;
3720
3859
  }
3721
3860
  }
3861
+ // -----------------------------------------------------------------------
3862
+ // Connection Lifecycle
3863
+ // -----------------------------------------------------------------------
3722
3864
  /**
3723
- * Reconnect to an MCP server — tears down the active client transport/connection
3724
- * and creates a fresh connection while reusing the existing session credentials in a single RPC call.
3865
+ * Initiates a connection to a new MCP server.
3866
+ *
3867
+ * If a session for the same `serverId` or `serverUrl` already exists and
3868
+ * is still in a `pending` (OAuth) state, the existing session is resumed
3869
+ * instead of creating a duplicate.
3870
+ *
3871
+ * `UnauthorizedError` is treated as a pending-auth state and returned as
3872
+ * a successful result (the client will then redirect to the auth URL).
3725
3873
  */
3726
- async reconnect(params) {
3727
- const { serverId: rawServerId, serverName, serverUrl, callbackUrl, transportType, clientId, clientSecret } = params;
3874
+ async connect(params) {
3728
3875
  const headers = normalizeHeaders(params.headers);
3729
- const serverId = rawServerId && rawServerId.length <= 12 ? rawServerId : generateServerId();
3730
- const existingSessions = await sessions.list(this.userId);
3731
- const duplicate = existingSessions.find(
3732
- (s) => s.serverId === serverId || s.serverUrl === serverUrl
3733
- );
3734
- const sessionId = duplicate ? duplicate.sessionId : await sessions.generateSessionId();
3735
- if (duplicate) {
3736
- const existingClient = this.clients.get(duplicate.sessionId);
3737
- if (existingClient) {
3738
- await existingClient.disconnect();
3739
- this.clients.delete(duplicate.sessionId);
3876
+ const serverId = this.normalizeServerId(params.serverId);
3877
+ const existing = await this.findExistingSession(serverId, params.serverUrl);
3878
+ if (existing) {
3879
+ if (existing.status === "pending") {
3880
+ return this.getSession({ sessionId: existing.sessionId }).then(() => ({
3881
+ sessionId: existing.sessionId,
3882
+ success: true
3883
+ }));
3740
3884
  }
3885
+ throw new Error(
3886
+ `Connection already exists for server: ${existing.serverUrl ?? existing.serverId} (${existing.serverName})`
3887
+ );
3741
3888
  }
3742
- try {
3743
- const clientMetadata = await this.getResolvedClientMetadata();
3744
- const client = new MCPClient({
3745
- userId: this.userId,
3746
- sessionId,
3747
- serverId,
3748
- serverName,
3749
- serverUrl,
3750
- callbackUrl,
3751
- transportType,
3752
- headers,
3753
- clientId,
3754
- clientSecret,
3755
- ...clientMetadata
3756
- });
3757
- this.clients.set(sessionId, client);
3758
- client.onConnectionEvent((event) => {
3759
- this.emitConnectionEvent(event);
3760
- });
3761
- client.onObservabilityEvent((event) => {
3762
- this.sendEvent(event);
3763
- });
3764
- await client.connect();
3765
- await this.listPolicyFilteredTools(sessionId);
3766
- return { sessionId, success: true };
3767
- } catch (error) {
3768
- if (error instanceof UnauthorizedError) {
3769
- this.clients.delete(sessionId);
3770
- return { sessionId, success: true };
3889
+ const sessionId = await sessions.generateSessionId();
3890
+ const metadata = await this.getResolvedClientMetadata();
3891
+ const client = new MCPClient({
3892
+ userId: this.userId,
3893
+ sessionId,
3894
+ serverId,
3895
+ serverName: params.serverName,
3896
+ serverUrl: params.serverUrl,
3897
+ callbackUrl: params.callbackUrl,
3898
+ transportType: params.transportType,
3899
+ headers,
3900
+ sessionStore: this.observedStore,
3901
+ ...metadata
3902
+ });
3903
+ this.cacheClient(sessionId, client);
3904
+ return this.connectAndDiscover(client, sessionId, serverId);
3905
+ }
3906
+ /**
3907
+ * Reconnects to an MCP server by tearing down the active transport if one
3908
+ * exists and instantiating a fresh connection, reusing the existing session
3909
+ * (and its stored credentials / DCR client info) from the database.
3910
+ */
3911
+ async reconnect(params) {
3912
+ const headers = normalizeHeaders(params.headers);
3913
+ const serverId = this.normalizeServerId(params.serverId);
3914
+ const existing = await this.findExistingSession(serverId, params.serverUrl);
3915
+ const sessionId = existing ? existing.sessionId : await sessions.generateSessionId();
3916
+ if (existing) {
3917
+ const staleClient = this.clients.get(existing.sessionId);
3918
+ if (staleClient) {
3919
+ await staleClient.disconnect();
3920
+ this.clients.delete(existing.sessionId);
3771
3921
  }
3772
- this.emitConnectionEvent({
3773
- type: "error",
3774
- sessionId,
3775
- serverId,
3776
- error: error instanceof Error ? error.message : "Connection failed",
3777
- errorType: "connection",
3778
- timestamp: Date.now()
3779
- });
3780
- this.clients.delete(sessionId);
3781
- throw error;
3782
3922
  }
3923
+ const metadata = await this.getResolvedClientMetadata();
3924
+ const client = new MCPClient({
3925
+ userId: this.userId,
3926
+ sessionId,
3927
+ serverId,
3928
+ serverName: params.serverName,
3929
+ serverUrl: params.serverUrl,
3930
+ callbackUrl: params.callbackUrl,
3931
+ transportType: params.transportType,
3932
+ headers,
3933
+ sessionStore: this.observedStore,
3934
+ ...metadata
3935
+ });
3936
+ this.cacheClient(sessionId, client);
3937
+ return this.connectAndDiscover(client, sessionId, serverId);
3783
3938
  }
3784
3939
  /**
3785
- * Disconnect from an MCP server
3940
+ * Disconnects from an MCP server.
3941
+ *
3942
+ * If an active in-memory transport exists it delegates to `clearSession()`
3943
+ * (which sends the server an HTTP DELETE per the Streamable spec). If no
3944
+ * active transport is available (orphaned session from a failed OAuth flow),
3945
+ * the session row is removed directly from storage.
3786
3946
  */
3787
3947
  async disconnect(params) {
3788
- const { sessionId } = params;
3789
- const client = this.clients.get(sessionId);
3948
+ const client = this.clients.get(params.sessionId);
3790
3949
  if (client) {
3791
3950
  await client.clearSession();
3792
- this.clients.delete(sessionId);
3951
+ this.clients.delete(params.sessionId);
3793
3952
  } else {
3794
- await sessions.delete(this.userId, sessionId);
3953
+ await sessions.delete(this.userId, params.sessionId);
3795
3954
  }
3796
3955
  return { success: true };
3797
3956
  }
3957
+ // -----------------------------------------------------------------------
3958
+ // OAuth
3959
+ // -----------------------------------------------------------------------
3798
3960
  /**
3799
- * Get an existing client or create and connect a new one for the session.
3961
+ * Completes the OAuth 2.1 authorization code flow for a pending session.
3962
+ *
3963
+ * Loads the stored session (with credentials), creates a fresh `MCPClient`,
3964
+ * and calls `finishAuth` inside a {@link runWithCodeVerifierState} context
3965
+ * so the PKCE code verifier is available without a DB read.
3966
+ *
3967
+ * The session's stored `transportType` is intentionally **omitted** from the
3968
+ * client config — this lets `MCPClient` auto-negotiate (try Streamable HTTP
3969
+ * first, fall back to SSE), which is required for servers like Neon that
3970
+ * only support SSE transport.
3800
3971
  */
3801
- async getOrCreateClient(sessionId) {
3802
- const existing = this.clients.get(sessionId);
3803
- if (existing) {
3804
- return existing;
3805
- }
3806
- const session = await sessions.get(this.userId, sessionId);
3807
- if (!session) {
3808
- throw new Error("Session not found");
3972
+ async finishAuth(params) {
3973
+ const parsed = parseOAuthState(params.state);
3974
+ const sessionId = parsed?.sessionId ?? params.state;
3975
+ const session = await this.requireSession(sessionId);
3976
+ try {
3977
+ const clientId = session.clientId ?? void 0;
3978
+ const clientSecret = session.clientInformation?.client_secret ?? void 0;
3979
+ const client = new MCPClient({
3980
+ userId: this.userId,
3981
+ sessionId,
3982
+ serverId: session.serverId,
3983
+ serverName: session.serverName,
3984
+ serverUrl: session.serverUrl,
3985
+ callbackUrl: session.callbackUrl,
3986
+ headers: session.headers,
3987
+ clientId,
3988
+ clientSecret,
3989
+ hasSession: true,
3990
+ cachedCredentials: { tokens: session.tokens ?? void 0 },
3991
+ sessionStore: this.observedStore
3992
+ });
3993
+ this.attachClientEvents(client);
3994
+ await runWithCodeVerifierState(
3995
+ session.codeVerifier ?? "",
3996
+ "S256",
3997
+ () => client.finishAuth(params.code, params.state)
3998
+ );
3999
+ this.clients.set(sessionId, client);
4000
+ const { result } = await this.listPolicyFilteredTools(sessionId);
4001
+ return { success: true, toolCount: result.tools.length };
4002
+ } catch (error) {
4003
+ this.sendEvent(connectionErrorEvent(sessionId, session.serverId, error, "auth"));
4004
+ throw error;
3809
4005
  }
3810
- const client = new MCPClient({
3811
- userId: this.userId,
3812
- sessionId,
3813
- // These fields are optional in MCPClient, but when rehydrating a known
3814
- // stored session on the server we pass them explicitly to preserve the
3815
- // original transport/connection metadata instead of relying on lazy
3816
- // reloading during initialize().
3817
- serverId: session.serverId,
3818
- serverName: session.serverName,
3819
- serverUrl: session.serverUrl,
3820
- callbackUrl: session.callbackUrl,
3821
- transportType: session.transportType,
3822
- headers: session.headers
3823
- });
3824
- client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3825
- client.onObservabilityEvent((event) => this.sendEvent(event));
3826
- await client.connect();
3827
- this.clients.set(sessionId, client);
3828
- return client;
3829
4006
  }
4007
+ // -----------------------------------------------------------------------
4008
+ // Tool Discovery & Policy
4009
+ // -----------------------------------------------------------------------
3830
4010
  /**
3831
- * Fetches all tools from the remote MCP server and emits a `tools_discovered` event.
4011
+ * Fetches the complete tool list from the remote MCP server and emits a
4012
+ * `tools_discovered` SSE event with two lists:
3832
4013
  *
3833
- * Two lists are always published together:
3834
- * - `tools` policy-filtered list that agents are allowed to call.
3835
- * - `allTools` — the complete, unfiltered list used by the management UI so
3836
- * that blocked tools still appear as checkboxes in the dialog.
4014
+ * - `tools` — policy-filtered (what agents are allowed to call)
4015
+ * - `allTools` complete, unfiltered (used by the management UI)
3837
4016
  *
3838
- * `fetchTools()` is called first (populates the in-memory cache), then
3839
- * `gateway.listTools()` re-uses that cache internally — so only one remote
3840
- * network round-trip is made regardless of how many callers follow.
4017
+ * Only a single network round-trip is made: `fetchTools()` populates the
4018
+ * in-memory cache, then `gateway.listTools()` reuses that cache.
3841
4019
  *
3842
- * @param sessionId - The session whose tools should be discovered.
3843
- * @returns The session record and the policy-filtered tool list.
3844
- * @throws {Error} When the session does not exist in the store.
4020
+ * @param sessionId - The session to discover tools for.
4021
+ * @returns The session record and the filtered tool result.
4022
+ * @throws {Error} If the session does not exist in storage.
3845
4023
  */
3846
4024
  async listPolicyFilteredTools(sessionId) {
3847
- const session = await sessions.get(this.userId, sessionId);
3848
- if (!session) {
3849
- throw new Error("Session not found");
3850
- }
4025
+ const session = await this.requireSession(sessionId);
3851
4026
  const client = await this.getOrCreateClient(sessionId);
3852
4027
  const allTools = await client.fetchTools().catch(() => ({ tools: [] }));
3853
4028
  const gateway = createToolPolicyGateway(this.userId, sessionId, client);
3854
4029
  const result = await gateway.listTools();
3855
- this.emitConnectionEvent({
4030
+ this.sendEvent({
3856
4031
  type: "tools_discovered",
3857
4032
  sessionId,
3858
4033
  serverId: session.serverId ?? "unknown",
@@ -3864,254 +4039,254 @@ var SSEConnectionManager = class {
3864
4039
  return { session, result };
3865
4040
  }
3866
4041
  /**
3867
- * Returns the policy-filtered tool list for a session (agent-facing).
3868
- * Internally re-uses `listPolicyFilteredTools` which also emits a
3869
- * `tools_discovered` SSE event to keep client state up to date.
4042
+ * Returns the policy-filtered tool list for a session.
4043
+ *
4044
+ * Delegates to {@link listPolicyFilteredTools}, which also emits a
4045
+ * `tools_discovered` SSE event to keep the client state current.
3870
4046
  */
3871
4047
  async listTools(params) {
3872
- const { sessionId } = params;
3873
- const { result } = await this.listPolicyFilteredTools(sessionId);
4048
+ const { result } = await this.listPolicyFilteredTools(params.sessionId);
3874
4049
  return { tools: result.tools };
3875
4050
  }
3876
4051
  /**
3877
- * Get all raw tools with effective access state for management UI.
4052
+ * Returns all raw tools annotated with their effective policy state
4053
+ * for display in the management UI.
3878
4054
  */
3879
4055
  async getToolPolicy(params) {
3880
- const { sessionId } = params;
3881
- const session = await sessions.get(this.userId, sessionId);
3882
- if (!session) {
3883
- throw new Error("Session not found");
3884
- }
3885
- const client = await this.getOrCreateClient(sessionId);
4056
+ const session = await this.requireSession(params.sessionId);
4057
+ const client = await this.getOrCreateClient(params.sessionId);
3886
4058
  const allTools = await client.fetchTools();
3887
- const toolPolicy = session.toolPolicy ?? {
4059
+ const policy = session.toolPolicy ?? {
3888
4060
  mode: "all",
3889
4061
  toolIds: [],
3890
4062
  updatedAt: session.updatedAt ?? session.createdAt
3891
4063
  };
3892
4064
  const serverId = session.serverId ?? "unknown";
3893
- const tools = allTools.tools.map((tool) => ({
3894
- ...tool,
3895
- toolId: createToolId(serverId, tool.name),
3896
- allowed: isToolAllowed(toolPolicy, tool.name, session.serverId)
4065
+ const tools = allTools.tools.map((t) => ({
4066
+ ...t,
4067
+ toolId: createToolId(serverId, t.name),
4068
+ allowed: isToolAllowed(policy, t.name, session.serverId)
3897
4069
  }));
3898
4070
  return {
3899
- toolPolicy,
4071
+ toolPolicy: policy,
3900
4072
  tools,
3901
4073
  toolCount: tools.length,
3902
- allowedToolCount: tools.filter((tool) => tool.allowed).length
4074
+ allowedToolCount: tools.filter((t) => t.allowed).length
3903
4075
  };
3904
4076
  }
3905
4077
  /**
3906
- * Persists a new tool access policy for a session and broadcasts the updated
3907
- * filtered tool list to all connected browser clients via a `tools_discovered` event.
3908
- *
3909
- * Both `tools` (policy-filtered) and `allTools` (complete list) are emitted
3910
- * so the management UI can immediately reflect the new checkbox states without
3911
- * an additional round-trip to the server.
4078
+ * Persists a new tool access policy and broadcasts the updated filtered
4079
+ * tool list via a `tools_discovered` SSE event.
3912
4080
  *
3913
- * @param params - Session ID and the new `{ mode, toolIds }` policy to apply.
3914
- * @throws {Error} When the session does not exist or the policy references unknown tool IDs.
4081
+ * @throws {Error} If the session does not exist or the policy references
4082
+ * tool IDs that don't match any known tool.
3915
4083
  */
3916
4084
  async setToolPolicy(params) {
3917
- const { sessionId } = params;
3918
- const session = await sessions.get(this.userId, sessionId);
3919
- if (!session) {
3920
- throw new Error("Session not found");
3921
- }
3922
- const client = await this.getOrCreateClient(sessionId);
4085
+ const session = await this.requireSession(params.sessionId);
4086
+ const client = await this.getOrCreateClient(params.sessionId);
3923
4087
  const allTools = await client.fetchTools();
3924
- const toolPolicy = normalizeToolPolicyForUpdate(params.toolPolicy);
3925
- validateToolPolicyAgainstTools(toolPolicy, allTools.tools, session.serverId);
3926
- await sessions.update(this.userId, sessionId, { toolPolicy });
3927
- const filteredTools = createToolPolicyGateway(this.userId, sessionId, client).filterTools({ ...session, toolPolicy }, allTools.tools);
3928
- this.emitConnectionEvent({
4088
+ const policy = normalizeToolPolicyForUpdate(params.toolPolicy);
4089
+ validateToolPolicyAgainstTools(policy, allTools.tools, session.serverId);
4090
+ await sessions.update(this.userId, params.sessionId, { toolPolicy: policy });
4091
+ const filtered = createToolPolicyGateway(
4092
+ this.userId,
4093
+ params.sessionId,
4094
+ client
4095
+ ).filterTools({ ...session, toolPolicy: policy }, allTools.tools);
4096
+ this.sendEvent({
3929
4097
  type: "tools_discovered",
3930
- sessionId,
4098
+ sessionId: params.sessionId,
3931
4099
  serverId: session.serverId ?? "unknown",
3932
- toolCount: filteredTools.length,
3933
- tools: filteredTools,
4100
+ toolCount: filtered.length,
4101
+ tools: filtered,
3934
4102
  allTools: allTools.tools,
3935
4103
  timestamp: Date.now()
3936
4104
  });
3937
- return {
3938
- success: true,
3939
- toolPolicy,
3940
- tools: filteredTools,
3941
- toolCount: filteredTools.length
3942
- };
4105
+ return { success: true, toolPolicy: policy, tools: filtered, toolCount: filtered.length };
3943
4106
  }
3944
4107
  /**
3945
- * Proxies a tool invocation to the remote MCP server.
3946
- * Resolves the client for the given session and delegates to the tool router.
3947
- */
3948
- async callTool(params) {
3949
- const { sessionId, toolName, toolArgs } = params;
3950
- const client = await this.getOrCreateClient(sessionId);
3951
- const result = await createToolPolicyGateway(this.userId, sessionId, client).callTool(toolName, toolArgs);
3952
- const meta = result._meta || {};
3953
- return {
3954
- ...result,
3955
- _meta: {
3956
- ...meta,
3957
- sessionId
3958
- }
3959
- };
3960
- }
3961
- /**
3962
- * Restore and validate an existing session
4108
+ * Enables or disables a session for agent tool discovery.
4109
+ *
4110
+ * Disabled sessions retain their OAuth tokens and connection metadata
4111
+ * but are hidden from `MultiSessionClient.connect()` and blocked from
4112
+ * RPC tool access. Re-enabling does not require re-authentication.
4113
+ *
4114
+ * @param params - `{ sessionId, enabled: boolean }`
4115
+ * @returns `{ success: true }`
4116
+ * @throws {Error} If the session does not exist.
3963
4117
  */
3964
- async getSession(params) {
3965
- const { sessionId } = params;
3966
- const session = await sessions.get(this.userId, sessionId);
3967
- if (!session) {
3968
- throw new Error("Session not found");
3969
- }
3970
- this.emitConnectionEvent({
3971
- type: "state_changed",
3972
- sessionId,
3973
- serverId: session.serverId ?? "unknown",
3974
- serverName: session.serverName ?? "Unknown",
3975
- serverUrl: session.serverUrl,
3976
- state: "VALIDATING",
3977
- previousState: "DISCONNECTED",
3978
- timestamp: Date.now()
3979
- });
3980
- try {
3981
- const clientMetadata = await this.getResolvedClientMetadata();
3982
- const client = new MCPClient({
3983
- userId: this.userId,
3984
- sessionId,
3985
- // These fields are optional in MCPClient, but when rehydrating a known
3986
- // stored session on the server we pass them explicitly to preserve the
3987
- // original transport/connection metadata instead of relying on lazy
3988
- // reloading during initialize().
3989
- serverId: session.serverId,
3990
- serverName: session.serverName,
3991
- serverUrl: session.serverUrl,
3992
- callbackUrl: session.callbackUrl,
3993
- transportType: session.transportType,
3994
- headers: session.headers,
3995
- ...clientMetadata
3996
- });
3997
- client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3998
- client.onObservabilityEvent((event) => this.sendEvent(event));
3999
- await client.connect();
4000
- this.clients.set(sessionId, client);
4001
- const { result: tools } = await this.listPolicyFilteredTools(sessionId);
4002
- return { success: true, toolCount: tools.tools.length };
4003
- } catch (error) {
4004
- this.emitConnectionEvent({
4005
- type: "error",
4006
- sessionId,
4007
- serverId: session.serverId ?? "unknown",
4008
- error: error instanceof Error ? error.message : "Validation failed",
4009
- errorType: "validation",
4010
- timestamp: Date.now()
4011
- });
4012
- throw error;
4013
- }
4118
+ async updateSession(params) {
4119
+ await this.requireSession(params.sessionId);
4120
+ await sessions.update(this.userId, params.sessionId, { enabled: params.enabled });
4121
+ return { success: true };
4014
4122
  }
4123
+ // -----------------------------------------------------------------------
4124
+ // Tool Execution
4125
+ // -----------------------------------------------------------------------
4015
4126
  /**
4016
- * Complete OAuth authorization flow
4127
+ * Proxies a tool invocation to the remote MCP server.
4128
+ *
4129
+ * Resolves (or creates) the transport for the given session, runs the call
4130
+ * through the tool-policy gateway, and injects `sessionId` into the result
4131
+ * metadata so the client can route the response without scanning all sessions.
4017
4132
  */
4018
- async finishAuth(params) {
4019
- const { code } = params;
4020
- const oauthState = params.state;
4021
- const parsedState = parseOAuthState(oauthState);
4022
- const sessionId = parsedState?.sessionId || oauthState;
4023
- const session = await sessions.get(this.userId, sessionId);
4024
- if (!session) {
4025
- throw new Error("Session not found");
4026
- }
4027
- try {
4028
- const client = new MCPClient({
4029
- userId: this.userId,
4030
- sessionId,
4031
- // These fields are optional in MCPClient, but when rehydrating a known
4032
- // stored session on the server we pass them explicitly to preserve the
4033
- // original connection metadata instead of relying on lazy
4034
- // reloading during initialize().
4035
- serverId: session.serverId,
4036
- serverName: session.serverName,
4037
- serverUrl: session.serverUrl,
4038
- callbackUrl: session.callbackUrl,
4039
- // NOTE: transportType is intentionally omitted here.
4040
- // The session's stored transportType is a placeholder ('streamable-http')
4041
- // set before transport negotiation. Omitting it lets MCPClient auto-negotiate
4042
- // (try streamable_http → SSE fallback), which is critical for servers like
4043
- // Neon that only support SSE transport.
4044
- headers: session.headers
4045
- });
4046
- client.onConnectionEvent((event) => this.emitConnectionEvent(event));
4047
- await client.finishAuth(code, oauthState);
4048
- this.clients.set(sessionId, client);
4049
- const { result: tools } = await this.listPolicyFilteredTools(sessionId);
4050
- return { success: true, toolCount: tools.tools.length };
4051
- } catch (error) {
4052
- this.emitConnectionEvent({
4053
- type: "error",
4054
- sessionId,
4055
- serverId: session.serverId ?? "unknown",
4056
- error: error instanceof Error ? error.message : "OAuth completion failed",
4057
- errorType: "auth",
4058
- timestamp: Date.now()
4059
- });
4060
- throw error;
4061
- }
4133
+ async callTool(params) {
4134
+ const client = await this.getOrCreateClient(params.sessionId);
4135
+ const result = await createToolPolicyGateway(
4136
+ this.userId,
4137
+ params.sessionId,
4138
+ client
4139
+ ).callTool(params.toolName, params.toolArgs);
4140
+ return { ...result, _meta: { ...result._meta ?? {}, sessionId: params.sessionId } };
4062
4141
  }
4063
- /**
4064
- * List prompts from a session
4065
- */
4142
+ // -----------------------------------------------------------------------
4143
+ // Prompts
4144
+ // -----------------------------------------------------------------------
4145
+ /** Lists all prompts exposed by the remote MCP server. */
4066
4146
  async listPrompts(params) {
4067
- const { sessionId } = params;
4068
- const client = await this.getOrCreateClient(sessionId);
4147
+ const client = await this.getOrCreateClient(params.sessionId);
4069
4148
  const result = await client.listPrompts();
4070
4149
  return { prompts: result.prompts };
4071
4150
  }
4072
- /**
4073
- * Get a specific prompt
4074
- */
4151
+ /** Retrieves a specific prompt by name with optional arguments. */
4075
4152
  async getPrompt(params) {
4076
- const { sessionId, name, args } = params;
4077
- const client = await this.getOrCreateClient(sessionId);
4078
- return await client.getPrompt(name, args);
4153
+ const client = await this.getOrCreateClient(params.sessionId);
4154
+ return client.getPrompt(params.name, params.args);
4079
4155
  }
4080
- /**
4081
- * List resources from a session
4082
- */
4156
+ // -----------------------------------------------------------------------
4157
+ // Resources
4158
+ // -----------------------------------------------------------------------
4159
+ /** Lists all resources exposed by the remote MCP server. */
4083
4160
  async listResources(params) {
4084
- const { sessionId } = params;
4085
- const client = await this.getOrCreateClient(sessionId);
4161
+ const client = await this.getOrCreateClient(params.sessionId);
4086
4162
  const result = await client.listResources();
4087
4163
  return { resources: result.resources };
4088
4164
  }
4165
+ /** Reads a specific resource identified by URI. */
4166
+ async readResource(params) {
4167
+ const client = await this.getOrCreateClient(params.sessionId);
4168
+ return client.readResource(params.uri);
4169
+ }
4170
+ // -----------------------------------------------------------------------
4171
+ // Internal Helpers
4172
+ // -----------------------------------------------------------------------
4173
+ /** Resolves client metadata: `getClientMetadata()` → `clientDefaults` → `{}`. */
4174
+ async getResolvedClientMetadata(request) {
4175
+ let metadata = this.options.clientDefaults ? { ...this.options.clientDefaults } : {};
4176
+ if (this.options.getClientMetadata) {
4177
+ metadata = { ...metadata, ...await this.options.getClientMetadata(request) };
4178
+ }
4179
+ return metadata;
4180
+ }
4181
+ /** Ensures the given session exists in storage and throws otherwise. */
4182
+ async requireSession(sessionId) {
4183
+ const session = await sessions.get(this.userId, sessionId, { includeCredentials: true });
4184
+ if (!session) throw new Error(`Session ${sessionId} not found`);
4185
+ return session;
4186
+ }
4187
+ /** Finds an existing session matching the given serverId or serverUrl. */
4188
+ async findExistingSession(serverId, serverUrl) {
4189
+ const all = await sessions.list(this.userId);
4190
+ return all.find((s) => s.serverId === serverId || s.serverUrl === serverUrl);
4191
+ }
4192
+ /** Normalizes a serverId to max 12 chars (DeepSeek/OpenAI 64-char tool-name limit). */
4193
+ normalizeServerId(raw) {
4194
+ return raw && raw.length <= 12 ? raw : generateServerId();
4195
+ }
4089
4196
  /**
4090
- * Read a specific resource
4197
+ * Returns the cached in-memory transport for `sessionId`, or creates one
4198
+ * from the persisted session row (with credentials) and connects it.
4091
4199
  */
4092
- async readResource(params) {
4093
- const { sessionId, uri } = params;
4094
- const client = await this.getOrCreateClient(sessionId);
4095
- return client.readResource(uri);
4200
+ async getOrCreateClient(sessionId) {
4201
+ const existing = this.clients.get(sessionId);
4202
+ if (existing) return existing;
4203
+ const session = await this.requireSession(sessionId);
4204
+ if (session.enabled === false) {
4205
+ throw new Error("Session is disabled \u2014 re-enable it via updateSession to access tools");
4206
+ }
4207
+ const client = this.restoreClient(session);
4208
+ this.attachClientEvents(client);
4209
+ await client.connect();
4210
+ this.clients.set(sessionId, client);
4211
+ return client;
4096
4212
  }
4097
4213
  /**
4098
- * Emit connection event
4214
+ * Builds an `MCPClient` from a stored session row.
4215
+ *
4216
+ * Extracts `clientId` and `clientSecret` from the session's credential
4217
+ * fields and passes `hasSession: true` + `cachedCredentials` so the client
4218
+ * can skip redundant existence checks and credential reads.
4099
4219
  */
4100
- emitConnectionEvent(event) {
4101
- this.sendEvent(event);
4220
+ restoreClient(session) {
4221
+ const clientId = session.clientId ?? void 0;
4222
+ const clientSecret = session.clientInformation?.client_secret ?? void 0;
4223
+ return new MCPClient({
4224
+ userId: this.userId,
4225
+ sessionId: session.sessionId,
4226
+ serverId: session.serverId,
4227
+ serverName: session.serverName,
4228
+ serverUrl: session.serverUrl,
4229
+ callbackUrl: session.callbackUrl,
4230
+ transportType: session.transportType,
4231
+ headers: session.headers,
4232
+ clientId,
4233
+ clientSecret,
4234
+ hasSession: true,
4235
+ cachedCredentials: { tokens: session.tokens ?? void 0 },
4236
+ sessionStore: this.observedStore
4237
+ });
4102
4238
  }
4103
4239
  /**
4104
- * Cleanup and close all connections
4240
+ * Wires connection and observability events from the client into the
4241
+ * unified observability channel. Connection events go to `sendEvent`
4242
+ * (SSE stream); observability events go to `emitObs` (user callback
4243
+ * + SSE stream).
4105
4244
  */
4106
- async dispose() {
4107
- this.isActive = false;
4108
- if (this.heartbeatTimer) {
4109
- clearInterval(this.heartbeatTimer);
4245
+ attachClientEvents(client) {
4246
+ client.onConnectionEvent((e) => this.sendEvent(e));
4247
+ client.onObservabilityEvent((e) => this.sendEvent(e));
4248
+ }
4249
+ /**
4250
+ * Registers the client in the in-memory cache and attaches its event
4251
+ * listeners to the unified observability channel.
4252
+ */
4253
+ cacheClient(sessionId, client) {
4254
+ this.attachClientEvents(client);
4255
+ this.clients.set(sessionId, client);
4256
+ }
4257
+ /**
4258
+ * Attempts a `client.connect()` and, on success, discovers tools.
4259
+ *
4260
+ * If the server responds with `UnauthorizedError`, the session is
4261
+ * treated as pending OAuth — a successful result is returned with
4262
+ * the sessionId so the browser client can redirect to the auth URL.
4263
+ *
4264
+ * For any other error an `error` connection event is emitted and the
4265
+ * client is removed from the in-memory cache before re-throwing.
4266
+ */
4267
+ async connectAndDiscover(client, sessionId, serverId) {
4268
+ try {
4269
+ await client.connect();
4270
+ await this.listPolicyFilteredTools(sessionId);
4271
+ return { sessionId, success: true };
4272
+ } catch (error) {
4273
+ if (error instanceof UnauthorizedError) {
4274
+ this.clients.delete(sessionId);
4275
+ return { sessionId, success: true };
4276
+ }
4277
+ this.sendEvent(connectionErrorEvent(sessionId, serverId, error, "connection"));
4278
+ this.clients.delete(sessionId);
4279
+ throw error;
4110
4280
  }
4111
- await Promise.all(
4112
- Array.from(this.clients.values()).map((client) => client.disconnect())
4113
- );
4114
- this.clients.clear();
4281
+ }
4282
+ /** Starts the periodic heartbeat timer. */
4283
+ startHeartbeat() {
4284
+ const ms = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS;
4285
+ this.heartbeatTimer = setInterval(() => {
4286
+ if (this.active) {
4287
+ this.sendEvent({ level: "debug", message: "heartbeat", timestamp: Date.now() });
4288
+ }
4289
+ }, ms);
4115
4290
  }
4116
4291
  };
4117
4292
  function createSSEHandler(options) {
@@ -4140,8 +4315,7 @@ function createSSEHandler(options) {
4140
4315
  });
4141
4316
  req.on("end", async () => {
4142
4317
  try {
4143
- const request = JSON.parse(body);
4144
- await manager.handleRequest(request);
4318
+ await manager.handleRequest(JSON.parse(body));
4145
4319
  } catch {
4146
4320
  }
4147
4321
  });
@@ -4347,6 +4521,9 @@ var SSEClient = class {
4347
4521
  async getToolPolicy(sessionId) {
4348
4522
  return this.sendRequest("getToolPolicy", { sessionId });
4349
4523
  }
4524
+ async updateSession(sessionId, enabled) {
4525
+ return this.sendRequest("updateSession", { sessionId, enabled });
4526
+ }
4350
4527
  async listTools(sessionId) {
4351
4528
  return this.sendRequest("listTools", { sessionId });
4352
4529
  }
@@ -6213,6 +6390,6 @@ function globToRegExp(pattern) {
6213
6390
  return new RegExp(regexPattern);
6214
6391
  }
6215
6392
 
6216
- export { APP_HOST_DEFAULTS, AppHost, AuthenticationError, ConfigurationError, ConnectionError, DEFAULT_CLIENT_NAME, DEFAULT_CLIENT_URI, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_LOGO_URI, DEFAULT_MCP_APP_CSP, DEFAULT_POLICY_URI, DORMANT_SESSION_EXPIRATION_MS, DORMANT_SESSION_EXPIRATION_SECONDS, DisposableStore, Emitter, InvalidStateError, MCPClient, MCP_CLIENT_NAME, MCP_CLIENT_VERSION, McpError, MultiSessionClient, NotConnectedError, PENDING_SESSION_EXPIRATION_SECONDS, REDIS_KEY_PREFIX, RpcErrorCodes, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, SOFTWARE_ID, SOFTWARE_VERSION, SSEClient, SSEConnectionManager, STATE_EXPIRATION_MS, SchemaCompressor, SessionNotFoundError, SessionValidationError, StorageOAuthClientProvider, ToolExecutionError, ToolIndex, ToolRouter, UnauthorizedError, createExecuteToolDefinition, createGetSchemaToolDefinition, createListServersToolDefinition, createNextMcpHandler, createRegexSearchToolDefinition, createSSEHandler, createSearchToolDefinition, executeMetaTool, findToolByName, getToolUiResourceUri, isCallToolSuccess, isConnectAuthRequired, isConnectError, isConnectSuccess, isListToolsSuccess, isMetaTool, onSessionMutation, resolveMetaToolProxy, sanitizeServerLabel, sessions };
6393
+ export { APP_HOST_DEFAULTS, AppHost, AuthenticationError, ConfigurationError, ConnectionError, DEFAULT_CLIENT_NAME, DEFAULT_CLIENT_URI, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_LOGO_URI, DEFAULT_MCP_APP_CSP, DEFAULT_POLICY_URI, DORMANT_SESSION_EXPIRATION_MS, DORMANT_SESSION_EXPIRATION_SECONDS, DisposableStore, Emitter, InvalidStateError, MCPClient, MCP_CLIENT_NAME, MCP_CLIENT_VERSION, McpError, MultiSessionClient, NotConnectedError, PENDING_SESSION_EXPIRATION_SECONDS, REDIS_KEY_PREFIX, RpcErrorCodes, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, SOFTWARE_ID, SOFTWARE_VERSION, SSEClient, SSEConnectionManager, STATE_EXPIRATION_MS, SchemaCompressor, SessionNotFoundError, SessionValidationError, StorageOAuthClientProvider, ToolExecutionError, ToolIndex, ToolRouter, UnauthorizedError, createExecuteToolDefinition, createGetSchemaToolDefinition, createListServersToolDefinition, createNextMcpHandler, createRegexSearchToolDefinition, createSSEHandler, createSearchToolDefinition, decryptObject, encryptObject, executeMetaTool, findToolByName, getToolUiResourceUri, isCallToolSuccess, isConnectAuthRequired, isConnectError, isConnectSuccess, isListToolsSuccess, isMetaTool, onSessionMutation, resolveMetaToolProxy, sanitizeServerLabel, sessions, withDbObservability };
6217
6394
  //# sourceMappingURL=index.mjs.map
6218
6395
  //# sourceMappingURL=index.mjs.map