@mcp-ts/sdk 1.3.5 → 1.3.7

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 (70) hide show
  1. package/dist/adapters/agui-adapter.d.mts +1 -1
  2. package/dist/adapters/agui-adapter.d.ts +1 -1
  3. package/dist/adapters/agui-adapter.js +2 -2
  4. package/dist/adapters/agui-adapter.js.map +1 -1
  5. package/dist/adapters/agui-adapter.mjs +2 -2
  6. package/dist/adapters/agui-adapter.mjs.map +1 -1
  7. package/dist/adapters/agui-middleware.d.mts +1 -1
  8. package/dist/adapters/agui-middleware.d.ts +1 -1
  9. package/dist/adapters/agui-middleware.js.map +1 -1
  10. package/dist/adapters/agui-middleware.mjs.map +1 -1
  11. package/dist/adapters/ai-adapter.d.mts +1 -1
  12. package/dist/adapters/ai-adapter.d.ts +1 -1
  13. package/dist/adapters/ai-adapter.js +1 -1
  14. package/dist/adapters/ai-adapter.js.map +1 -1
  15. package/dist/adapters/ai-adapter.mjs +1 -1
  16. package/dist/adapters/ai-adapter.mjs.map +1 -1
  17. package/dist/adapters/langchain-adapter.d.mts +1 -1
  18. package/dist/adapters/langchain-adapter.d.ts +1 -1
  19. package/dist/adapters/langchain-adapter.js +1 -1
  20. package/dist/adapters/langchain-adapter.js.map +1 -1
  21. package/dist/adapters/langchain-adapter.mjs +1 -1
  22. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  23. package/dist/adapters/mastra-adapter.d.mts +1 -1
  24. package/dist/adapters/mastra-adapter.d.ts +1 -1
  25. package/dist/adapters/mastra-adapter.js +1 -1
  26. package/dist/adapters/mastra-adapter.js.map +1 -1
  27. package/dist/adapters/mastra-adapter.mjs +1 -1
  28. package/dist/adapters/mastra-adapter.mjs.map +1 -1
  29. package/dist/bin/mcp-ts.d.mts +1 -0
  30. package/dist/bin/mcp-ts.d.ts +1 -0
  31. package/dist/bin/mcp-ts.js +105 -0
  32. package/dist/bin/mcp-ts.js.map +1 -0
  33. package/dist/bin/mcp-ts.mjs +82 -0
  34. package/dist/bin/mcp-ts.mjs.map +1 -0
  35. package/dist/index.d.mts +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +411 -90
  38. package/dist/index.js.map +1 -1
  39. package/dist/index.mjs +350 -91
  40. package/dist/index.mjs.map +1 -1
  41. package/dist/{multi-session-client-BYLarghq.d.ts → multi-session-client-CHE8QpVE.d.ts} +75 -5
  42. package/dist/{multi-session-client-CzhMkE0k.d.mts → multi-session-client-CQsRbxYI.d.mts} +75 -5
  43. package/dist/server/index.d.mts +1 -1
  44. package/dist/server/index.d.ts +1 -1
  45. package/dist/server/index.js +394 -90
  46. package/dist/server/index.js.map +1 -1
  47. package/dist/server/index.mjs +350 -91
  48. package/dist/server/index.mjs.map +1 -1
  49. package/dist/shared/index.js +10 -2
  50. package/dist/shared/index.js.map +1 -1
  51. package/dist/shared/index.mjs +10 -2
  52. package/dist/shared/index.mjs.map +1 -1
  53. package/package.json +19 -6
  54. package/src/adapters/agui-adapter.ts +222 -222
  55. package/src/adapters/ai-adapter.ts +115 -115
  56. package/src/adapters/langchain-adapter.ts +127 -127
  57. package/src/adapters/mastra-adapter.ts +126 -126
  58. package/src/bin/mcp-ts.ts +102 -0
  59. package/src/server/handlers/nextjs-handler.ts +12 -12
  60. package/src/server/handlers/sse-handler.ts +61 -61
  61. package/src/server/mcp/multi-session-client.ts +135 -39
  62. package/src/server/storage/file-backend.ts +4 -16
  63. package/src/server/storage/index.ts +68 -25
  64. package/src/server/storage/memory-backend.ts +7 -16
  65. package/src/server/storage/redis-backend.ts +12 -16
  66. package/src/server/storage/sqlite-backend.ts +3 -6
  67. package/src/server/storage/supabase-backend.ts +228 -0
  68. package/src/shared/event-routing.ts +28 -28
  69. package/src/shared/utils.ts +22 -0
  70. package/supabase/migrations/20260330195700_install_mcp_sessions.sql +84 -0
@@ -123,8 +123,6 @@ var SOFTWARE_ID = "@mcp-ts";
123
123
  var SOFTWARE_VERSION = "1.3.4";
124
124
  var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
125
125
  var MCP_CLIENT_VERSION = "2.0";
126
-
127
- // src/server/storage/redis-backend.ts
128
126
  var firstChar = customAlphabet(
129
127
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
130
128
  1
@@ -133,6 +131,18 @@ var rest = customAlphabet(
133
131
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
134
132
  11
135
133
  );
134
+ function sanitizeServerLabel(name) {
135
+ let sanitized = name.replace(/[^a-zA-Z0-9-_]/g, "_").replace(/_{2,}/g, "_").toLowerCase();
136
+ if (!/^[a-zA-Z]/.test(sanitized)) {
137
+ sanitized = "s_" + sanitized;
138
+ }
139
+ return sanitized;
140
+ }
141
+ function generateSessionId() {
142
+ return firstChar() + rest();
143
+ }
144
+
145
+ // src/server/storage/redis-backend.ts
136
146
  var RedisStorageBackend = class {
137
147
  constructor(redis2) {
138
148
  this.redis = redis2;
@@ -141,6 +151,14 @@ var RedisStorageBackend = class {
141
151
  __publicField(this, "IDENTITY_KEY_PREFIX", "mcp:identity:");
142
152
  __publicField(this, "IDENTITY_KEY_SUFFIX", ":sessions");
143
153
  }
154
+ async init() {
155
+ try {
156
+ await this.redis.ping();
157
+ console.log("[mcp-ts][Storage] Redis: \u2713 Connected to server.");
158
+ } catch (error) {
159
+ throw new Error(`[RedisStorage] Failed to connect to Redis: ${error.message}`);
160
+ }
161
+ }
144
162
  /**
145
163
  * Generates Redis key for a specific session
146
164
  * @private
@@ -183,7 +201,7 @@ var RedisStorageBackend = class {
183
201
  return Array.from(keys);
184
202
  }
185
203
  generateSessionId() {
186
- return firstChar() + rest();
204
+ return generateSessionId();
187
205
  }
188
206
  async createSession(session, ttl) {
189
207
  const { sessionId, identity } = session;
@@ -351,14 +369,8 @@ var RedisStorageBackend = class {
351
369
  }
352
370
  }
353
371
  };
354
- var firstChar2 = customAlphabet(
355
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
356
- 1
357
- );
358
- var rest2 = customAlphabet(
359
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
360
- 11
361
- );
372
+
373
+ // src/server/storage/memory-backend.ts
362
374
  var MemoryStorageBackend = class {
363
375
  constructor() {
364
376
  // Map<identity:sessionId, SessionData>
@@ -366,11 +378,14 @@ var MemoryStorageBackend = class {
366
378
  // Map<identity, Set<sessionId>>
367
379
  __publicField(this, "identitySessions", /* @__PURE__ */ new Map());
368
380
  }
381
+ async init() {
382
+ console.log("[mcp-ts][Storage] Memory: \u2713 internal memory store active.");
383
+ }
369
384
  getSessionKey(identity, sessionId) {
370
385
  return `${identity}:${sessionId}`;
371
386
  }
372
387
  generateSessionId() {
373
- return firstChar2() + rest2();
388
+ return generateSessionId();
374
389
  }
375
390
  async createSession(session, ttl) {
376
391
  const { sessionId, identity } = session;
@@ -441,14 +456,6 @@ var MemoryStorageBackend = class {
441
456
  async disconnect() {
442
457
  }
443
458
  };
444
- var firstChar3 = customAlphabet(
445
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
446
- 1
447
- );
448
- var rest3 = customAlphabet(
449
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
450
- 11
451
- );
452
459
  var FileStorageBackend = class {
453
460
  /**
454
461
  * @param options.path Path to the JSON file storage (default: ./sessions.json)
@@ -485,6 +492,7 @@ var FileStorageBackend = class {
485
492
  }
486
493
  }
487
494
  this.initialized = true;
495
+ console.log(`[mcp-ts][Storage] File: \u2713 storage directory at ${path.dirname(this.filePath)} verified.`);
488
496
  }
489
497
  async ensureInitialized() {
490
498
  if (!this.initialized) await this.init();
@@ -498,7 +506,7 @@ var FileStorageBackend = class {
498
506
  return `${identity}:${sessionId}`;
499
507
  }
500
508
  generateSessionId() {
501
- return firstChar3() + rest3();
509
+ return generateSessionId();
502
510
  }
503
511
  async createSession(session, ttl) {
504
512
  await this.ensureInitialized();
@@ -589,6 +597,7 @@ var SqliteStorage = class {
589
597
  CREATE INDEX IF NOT EXISTS idx_${this.table}_identity ON ${this.table}(identity);
590
598
  `);
591
599
  this.initialized = true;
600
+ console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
592
601
  } catch (error) {
593
602
  if (error.code === "MODULE_NOT_FOUND" || error.message?.includes("better-sqlite3")) {
594
603
  throw new Error(
@@ -604,12 +613,7 @@ var SqliteStorage = class {
604
613
  }
605
614
  }
606
615
  generateSessionId() {
607
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
608
- let result = "";
609
- for (let i = 0; i < 32; i++) {
610
- result += chars.charAt(Math.floor(Math.random() * chars.length));
611
- }
612
- return result;
616
+ return generateSessionId();
613
617
  }
614
618
  async createSession(session, ttl) {
615
619
  this.ensureInitialized();
@@ -704,6 +708,157 @@ var SqliteStorage = class {
704
708
  }
705
709
  };
706
710
 
711
+ // src/server/storage/supabase-backend.ts
712
+ var SupabaseStorageBackend = class {
713
+ constructor(supabase) {
714
+ this.supabase = supabase;
715
+ __publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
716
+ }
717
+ async init() {
718
+ const { error } = await this.supabase.from("mcp_sessions").select("session_id").limit(0);
719
+ if (error) {
720
+ if (error.code === "42P01") {
721
+ throw new Error(
722
+ '[SupabaseStorage] Table "mcp_sessions" not found in your database. Please run "npx mcp-ts supabase-init" in your project to set up the required table and RLS policies.'
723
+ );
724
+ }
725
+ throw new Error(`[SupabaseStorage] Initialization check failed: ${error.message}`);
726
+ }
727
+ console.log('[mcp-ts][Storage] Supabase: \u2713 "mcp_sessions" table verified.');
728
+ }
729
+ generateSessionId() {
730
+ return generateSessionId();
731
+ }
732
+ mapRowToSessionData(row) {
733
+ return {
734
+ sessionId: row.session_id,
735
+ serverId: row.server_id,
736
+ serverName: row.server_name,
737
+ serverUrl: row.server_url,
738
+ transportType: row.transport_type,
739
+ callbackUrl: row.callback_url,
740
+ createdAt: new Date(row.created_at).getTime(),
741
+ identity: row.identity,
742
+ headers: row.headers,
743
+ active: row.active,
744
+ clientInformation: row.client_information,
745
+ tokens: row.tokens,
746
+ codeVerifier: row.code_verifier,
747
+ clientId: row.client_id
748
+ };
749
+ }
750
+ async createSession(session, ttl) {
751
+ const { sessionId, identity } = session;
752
+ if (!sessionId || !identity) throw new Error("identity and sessionId required");
753
+ const effectiveTtl = ttl ?? this.DEFAULT_TTL;
754
+ const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
755
+ const { error } = await this.supabase.from("mcp_sessions").insert({
756
+ session_id: sessionId,
757
+ user_id: identity,
758
+ // Maps user_id to identity to support RLS using auth.uid()
759
+ server_id: session.serverId,
760
+ server_name: session.serverName,
761
+ server_url: session.serverUrl,
762
+ transport_type: session.transportType,
763
+ callback_url: session.callbackUrl,
764
+ created_at: new Date(session.createdAt || Date.now()).toISOString(),
765
+ identity,
766
+ headers: session.headers,
767
+ active: session.active ?? false,
768
+ client_information: session.clientInformation,
769
+ tokens: session.tokens,
770
+ code_verifier: session.codeVerifier,
771
+ client_id: session.clientId,
772
+ expires_at: expiresAt
773
+ });
774
+ if (error) {
775
+ if (error.code === "23505") {
776
+ throw new Error(`Session ${sessionId} already exists`);
777
+ }
778
+ throw new Error(`Failed to create session in Supabase: ${error.message}`);
779
+ }
780
+ }
781
+ async updateSession(identity, sessionId, data, ttl) {
782
+ const effectiveTtl = ttl ?? this.DEFAULT_TTL;
783
+ const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
784
+ const updateData = {
785
+ expires_at: expiresAt,
786
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
787
+ };
788
+ if ("serverId" in data) updateData.server_id = data.serverId;
789
+ if ("serverName" in data) updateData.server_name = data.serverName;
790
+ if ("serverUrl" in data) updateData.server_url = data.serverUrl;
791
+ if ("transportType" in data) updateData.transport_type = data.transportType;
792
+ if ("callbackUrl" in data) updateData.callback_url = data.callbackUrl;
793
+ if ("active" in data) updateData.active = data.active;
794
+ if ("headers" in data) updateData.headers = data.headers;
795
+ if ("clientInformation" in data) updateData.client_information = data.clientInformation;
796
+ if ("tokens" in data) updateData.tokens = data.tokens;
797
+ if ("codeVerifier" in data) updateData.code_verifier = data.codeVerifier;
798
+ if ("clientId" in data) updateData.client_id = data.clientId;
799
+ const { data: updatedRows, error } = await this.supabase.from("mcp_sessions").update(updateData).eq("identity", identity).eq("session_id", sessionId).select("id");
800
+ if (error) {
801
+ throw new Error(`Failed to update session: ${error.message}`);
802
+ }
803
+ if (!updatedRows || updatedRows.length === 0) {
804
+ throw new Error(`Session ${sessionId} not found for identity ${identity}`);
805
+ }
806
+ }
807
+ async getSession(identity, sessionId) {
808
+ const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("identity", identity).eq("session_id", sessionId).maybeSingle();
809
+ if (error) {
810
+ console.error("[SupabaseStorage] Failed to get session:", error);
811
+ return null;
812
+ }
813
+ if (!data) return null;
814
+ return this.mapRowToSessionData(data);
815
+ }
816
+ async getIdentitySessionsData(identity) {
817
+ const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("identity", identity);
818
+ if (error) {
819
+ console.error(`[SupabaseStorage] Failed to get session data for ${identity}:`, error);
820
+ return [];
821
+ }
822
+ return data.map((row) => this.mapRowToSessionData(row));
823
+ }
824
+ async removeSession(identity, sessionId) {
825
+ const { error } = await this.supabase.from("mcp_sessions").delete().eq("identity", identity).eq("session_id", sessionId);
826
+ if (error) {
827
+ console.error("[SupabaseStorage] Failed to remove session:", error);
828
+ }
829
+ }
830
+ async getIdentityMcpSessions(identity) {
831
+ const { data, error } = await this.supabase.from("mcp_sessions").select("session_id").eq("identity", identity);
832
+ if (error) {
833
+ console.error(`[SupabaseStorage] Failed to get sessions for ${identity}:`, error);
834
+ return [];
835
+ }
836
+ return data.map((row) => row.session_id);
837
+ }
838
+ async getAllSessionIds() {
839
+ const { data, error } = await this.supabase.from("mcp_sessions").select("session_id");
840
+ if (error) {
841
+ console.error("[SupabaseStorage] Failed to get all sessions:", error);
842
+ return [];
843
+ }
844
+ return data.map((row) => row.session_id);
845
+ }
846
+ async clearAll() {
847
+ const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
848
+ if (error) {
849
+ console.error("[SupabaseStorage] Failed to clear sessions:", error);
850
+ }
851
+ }
852
+ async cleanupExpiredSessions() {
853
+ const { error } = await this.supabase.from("mcp_sessions").delete().lt("expires_at", (/* @__PURE__ */ new Date()).toISOString());
854
+ if (error) {
855
+ console.error("[SupabaseStorage] Failed to cleanup expired sessions:", error);
856
+ }
857
+ }
858
+ async disconnect() {
859
+ }
860
+ };
861
+
707
862
  // src/server/storage/index.ts
708
863
  var storageInstance = null;
709
864
  var storagePromise = null;
@@ -722,53 +877,85 @@ async function createStorage() {
722
877
  try {
723
878
  const { getRedis: getRedis2 } = await Promise.resolve().then(() => (init_redis(), redis_exports));
724
879
  const redis2 = await getRedis2();
725
- console.log("[Storage] Using Redis storage (Explicit)");
726
- return new RedisStorageBackend(redis2);
880
+ console.log('[mcp-ts][Storage] Explicit selection: "redis"');
881
+ return await initializeStorage(new RedisStorageBackend(redis2));
727
882
  } catch (error) {
728
- console.error("[Storage] Failed to initialize Redis:", error.message);
729
- console.log("[Storage] Falling back to In-Memory storage");
730
- return new MemoryStorageBackend();
883
+ console.error("[mcp-ts][Storage] Failed to initialize Redis:", error.message);
884
+ console.log("[mcp-ts][Storage] Falling back to In-Memory storage");
885
+ return await initializeStorage(new MemoryStorageBackend());
731
886
  }
732
887
  }
733
888
  if (type === "file") {
734
889
  const filePath = process.env.MCP_TS_STORAGE_FILE;
735
- if (!filePath) {
736
- console.warn('[Storage] MCP_TS_STORAGE_TYPE is "file" but MCP_TS_STORAGE_FILE is missing');
737
- }
738
- console.log(`[Storage] Using File storage (${filePath}) (Explicit)`);
890
+ console.log(`[mcp-ts][Storage] Explicit selection: "file" (${filePath || "default"})`);
739
891
  return await initializeStorage(new FileStorageBackend({ path: filePath }));
740
892
  }
741
893
  if (type === "sqlite") {
742
894
  const dbPath = process.env.MCP_TS_STORAGE_SQLITE_PATH;
743
- console.log(`[Storage] Using SQLite storage (${dbPath || "default"}) (Explicit)`);
895
+ console.log(`[mcp-ts][Storage] Explicit selection: "sqlite" (${dbPath || "default"})`);
744
896
  return await initializeStorage(new SqliteStorage({ path: dbPath }));
745
897
  }
898
+ if (type === "supabase") {
899
+ const url = process.env.SUPABASE_URL;
900
+ const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_ANON_KEY;
901
+ if (!url || !key) {
902
+ console.warn('[mcp-ts][Storage] Explicit selection "supabase" requires SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY.');
903
+ } else {
904
+ if (!process.env.SUPABASE_SERVICE_ROLE_KEY) {
905
+ console.warn('[mcp-ts][Storage] \u26A0\uFE0F Warning: Using "SUPABASE_ANON_KEY" for server-side storage. You may encounter RLS policy violations. "SUPABASE_SERVICE_ROLE_KEY" is recommended.');
906
+ }
907
+ try {
908
+ const { createClient } = await import('@supabase/supabase-js');
909
+ const client = createClient(url, key);
910
+ console.log('[mcp-ts][Storage] Explicit selection: "supabase"');
911
+ return await initializeStorage(new SupabaseStorageBackend(client));
912
+ } catch (error) {
913
+ console.error("[mcp-ts][Storage] Failed to initialize Supabase:", error.message);
914
+ console.log("[mcp-ts][Storage] Falling back to In-Memory storage");
915
+ return await initializeStorage(new MemoryStorageBackend());
916
+ }
917
+ }
918
+ }
746
919
  if (type === "memory") {
747
- console.log("[Storage] Using In-Memory storage (Explicit)");
748
- return new MemoryStorageBackend();
920
+ console.log('[mcp-ts][Storage] Explicit selection: "memory"');
921
+ return await initializeStorage(new MemoryStorageBackend());
749
922
  }
750
923
  if (process.env.REDIS_URL) {
751
924
  try {
752
925
  const { getRedis: getRedis2 } = await Promise.resolve().then(() => (init_redis(), redis_exports));
753
926
  const redis2 = await getRedis2();
754
- console.log("[Storage] Auto-detected REDIS_URL. Using Redis storage.");
755
- return new RedisStorageBackend(redis2);
927
+ console.log('[mcp-ts][Storage] Auto-detection: "redis" (via REDIS_URL)');
928
+ return await initializeStorage(new RedisStorageBackend(redis2));
756
929
  } catch (error) {
757
- console.error("[Storage] Redis auto-detection failed:", error.message);
758
- console.log("[Storage] Falling back to In-Memory storage");
759
- return new MemoryStorageBackend();
930
+ console.error("[mcp-ts][Storage] Redis auto-detection failed:", error.message);
931
+ console.log("[mcp-ts][Storage] Falling back to next available backend");
760
932
  }
761
933
  }
762
934
  if (process.env.MCP_TS_STORAGE_FILE) {
763
- console.log(`[Storage] Auto-detected MCP_TS_STORAGE_FILE. Using File storage (${process.env.MCP_TS_STORAGE_FILE}).`);
935
+ console.log(`[mcp-ts][Storage] Auto-detection: "file" (${process.env.MCP_TS_STORAGE_FILE})`);
764
936
  return await initializeStorage(new FileStorageBackend({ path: process.env.MCP_TS_STORAGE_FILE }));
765
937
  }
766
938
  if (process.env.MCP_TS_STORAGE_SQLITE_PATH) {
767
- console.log(`[Storage] Auto-detected MCP_TS_STORAGE_SQLITE_PATH. Using SQLite storage (${process.env.MCP_TS_STORAGE_SQLITE_PATH}).`);
939
+ console.log(`[mcp-ts][Storage] Auto-detection: "sqlite" (${process.env.MCP_TS_STORAGE_SQLITE_PATH})`);
768
940
  return await initializeStorage(new SqliteStorage({ path: process.env.MCP_TS_STORAGE_SQLITE_PATH }));
769
941
  }
770
- console.log("[Storage] No storage configured. Using In-Memory storage (Default).");
771
- return new MemoryStorageBackend();
942
+ if (process.env.SUPABASE_URL && (process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_ANON_KEY)) {
943
+ try {
944
+ const { createClient } = await import('@supabase/supabase-js');
945
+ const url = process.env.SUPABASE_URL;
946
+ const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_ANON_KEY;
947
+ if (!process.env.SUPABASE_SERVICE_ROLE_KEY) {
948
+ console.warn('[mcp-ts][Storage] \u26A0\uFE0F Warning: Using "SUPABASE_ANON_KEY" for server-side storage. You may encounter RLS policy violations. "SUPABASE_SERVICE_ROLE_KEY" is recommended.');
949
+ }
950
+ const client = createClient(url, key);
951
+ console.log('[mcp-ts][Storage] Auto-detection: "supabase" (via SUPABASE_URL)');
952
+ return await initializeStorage(new SupabaseStorageBackend(client));
953
+ } catch (error) {
954
+ console.error("[mcp-ts][Storage] Supabase auto-detection failed:", error.message);
955
+ }
956
+ }
957
+ console.log('[mcp-ts][Storage] Defaulting to: "memory"');
958
+ return await initializeStorage(new MemoryStorageBackend());
772
959
  }
773
960
  async function getStorage() {
774
961
  if (storageInstance) {
@@ -978,15 +1165,6 @@ var StorageOAuthClientProvider = class {
978
1165
  }
979
1166
  };
980
1167
 
981
- // src/shared/utils.ts
982
- function sanitizeServerLabel(name) {
983
- let sanitized = name.replace(/[^a-zA-Z0-9-_]/g, "_").replace(/_{2,}/g, "_").toLowerCase();
984
- if (!/^[a-zA-Z]/.test(sanitized)) {
985
- sanitized = "s_" + sanitized;
986
- }
987
- return sanitized;
988
- }
989
-
990
1168
  // src/shared/events.ts
991
1169
  var Emitter = class {
992
1170
  constructor() {
@@ -1949,47 +2127,132 @@ var MCPClient = class _MCPClient {
1949
2127
  };
1950
2128
 
1951
2129
  // src/server/mcp/multi-session-client.ts
2130
+ var DEFAULT_TIMEOUT_MS = 15e3;
2131
+ var DEFAULT_MAX_RETRIES = 2;
2132
+ var DEFAULT_RETRY_DELAY_MS = 1e3;
2133
+ var CONNECTION_BATCH_SIZE = 5;
1952
2134
  var MultiSessionClient = class {
2135
+ /**
2136
+ * Creates a new MultiSessionClient for the given user identity.
2137
+ *
2138
+ * @param identity - A unique string identifying the user (e.g. user ID or email).
2139
+ * @param options - Optional tuning for connection timeout, retry count, and retry delay.
2140
+ * Falls back to sensible defaults if not provided.
2141
+ */
1953
2142
  constructor(identity, options = {}) {
1954
2143
  __publicField(this, "clients", []);
1955
2144
  __publicField(this, "identity");
1956
2145
  __publicField(this, "options");
2146
+ __publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
1957
2147
  this.identity = identity;
1958
2148
  this.options = {
1959
- timeout: 15e3,
1960
- maxRetries: 2,
1961
- retryDelay: 1e3,
2149
+ timeout: DEFAULT_TIMEOUT_MS,
2150
+ maxRetries: DEFAULT_MAX_RETRIES,
2151
+ retryDelay: DEFAULT_RETRY_DELAY_MS,
1962
2152
  ...options
1963
2153
  };
1964
2154
  }
2155
+ /**
2156
+ * Fetches all sessions for this identity from storage and returns only the
2157
+ * ones that are ready to connect.
2158
+ *
2159
+ * A session is considered connectable when:
2160
+ * - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
2161
+ * - Its `active` flag is not explicitly `false` — sessions with `active: false` are
2162
+ * either mid-OAuth flow, auth-pending, or previously failed. We skip those here
2163
+ * and let the OAuth flow complete separately before we try to reconnect them.
2164
+ *
2165
+ * Note: Sessions where `active` is `undefined` (legacy records) are included
2166
+ * for backwards compatibility.
2167
+ */
1965
2168
  async getActiveSessions() {
1966
2169
  const sessions = await storage.getIdentitySessionsData(this.identity);
1967
- console.log(
1968
- `[MultiSessionClient] All sessions for ${this.identity}:`,
1969
- sessions.map((s) => ({ sessionId: s.sessionId, serverId: s.serverId }))
2170
+ const valid = sessions.filter(
2171
+ (s) => s.serverId && s.serverUrl && s.callbackUrl && s.active !== false
2172
+ // exclude OAuth-pending / failed sessions
1970
2173
  );
1971
- const valid = sessions.filter((s) => s.serverId && s.serverUrl && s.callbackUrl);
1972
- console.log(`[MultiSessionClient] Filtered valid sessions:`, valid.length);
1973
2174
  return valid;
1974
2175
  }
2176
+ /**
2177
+ * Connects to a list of sessions in controlled batches of `CONNECTION_BATCH_SIZE`.
2178
+ *
2179
+ * Batching prevents overwhelming the event loop or external servers when a user
2180
+ * has many active MCP sessions (e.g. 20+ servers). Within each batch, sessions
2181
+ * are connected concurrently using `Promise.all` for speed.
2182
+ */
1975
2183
  async connectInBatches(sessions) {
1976
- const BATCH_SIZE = 5;
1977
- for (let i = 0; i < sessions.length; i += BATCH_SIZE) {
1978
- const batch = sessions.slice(i, i + BATCH_SIZE);
2184
+ for (let i = 0; i < sessions.length; i += CONNECTION_BATCH_SIZE) {
2185
+ const batch = sessions.slice(i, i + CONNECTION_BATCH_SIZE);
1979
2186
  await Promise.all(batch.map((session) => this.connectSession(session)));
1980
2187
  }
1981
2188
  }
2189
+ /**
2190
+ * Connects a single session, with built-in deduplication to prevent race conditions.
2191
+ *
2192
+ * - If a client for this session already exists and is connected, returns immediately.
2193
+ * - If a connection attempt for this session is already in-flight (e.g. from a
2194
+ * concurrent call), it joins the existing promise instead of starting a new one.
2195
+ * This is the key concurrency lock — the `connectionPromises` map acts as a
2196
+ * per-session mutex so we never spin up two physical connections for the same session.
2197
+ * - On completion (success or failure), the promise is cleaned up from the map.
2198
+ */
1982
2199
  async connectSession(session) {
1983
2200
  const existingClient = this.clients.find((c) => c.getSessionId() === session.sessionId);
1984
2201
  if (existingClient?.isConnected()) {
1985
2202
  return;
1986
2203
  }
1987
- const maxRetries = this.options.maxRetries ?? 2;
1988
- const retryDelay = this.options.retryDelay ?? 1e3;
2204
+ if (this.connectionPromises.has(session.sessionId)) {
2205
+ return this.connectionPromises.get(session.sessionId);
2206
+ }
2207
+ const connectPromise = this.establishConnectionWithRetries(session);
2208
+ this.connectionPromises.set(session.sessionId, connectPromise);
2209
+ try {
2210
+ await connectPromise;
2211
+ } finally {
2212
+ this.connectionPromises.delete(session.sessionId);
2213
+ }
2214
+ }
2215
+ /**
2216
+ * The core connection loop for a single session.
2217
+ *
2218
+ * Attempts to establish a physical MCP connection, retrying up to `maxRetries` times
2219
+ * if the connection fails. Each attempt:
2220
+ * 1. Creates a fresh `MCPClient` instance from the session data.
2221
+ * 2. Races the connect call against a timeout promise — if the server doesn't respond
2222
+ * within `timeoutMs`, the attempt is aborted and counted as a failure.
2223
+ * 3. On success, replaces any stale client entry for this session in the `clients` array.
2224
+ * 4. On failure, waits `retryDelay` ms before the next attempt.
2225
+ *
2226
+ * If all attempts are exhausted, logs an error and returns silently (does not throw),
2227
+ * so a single bad server doesn't block the rest of the batch from connecting.
2228
+ */
2229
+ async establishConnectionWithRetries(session) {
2230
+ const maxRetries = this.options.maxRetries ?? DEFAULT_MAX_RETRIES;
2231
+ const retryDelay = this.options.retryDelay ?? DEFAULT_RETRY_DELAY_MS;
1989
2232
  let lastError;
1990
2233
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
1991
2234
  try {
1992
- const client = await this.createAndConnectClient(session);
2235
+ const client = new MCPClient({
2236
+ identity: this.identity,
2237
+ sessionId: session.sessionId,
2238
+ serverId: session.serverId,
2239
+ serverUrl: session.serverUrl,
2240
+ callbackUrl: session.callbackUrl,
2241
+ serverName: session.serverName,
2242
+ transportType: session.transportType,
2243
+ headers: session.headers
2244
+ });
2245
+ const timeoutMs = this.options.timeout ?? DEFAULT_TIMEOUT_MS;
2246
+ let timeoutTimer;
2247
+ const timeoutPromise = new Promise((_, reject) => {
2248
+ timeoutTimer = setTimeout(() => reject(new Error(`Connection timed out after ${timeoutMs}ms`)), timeoutMs);
2249
+ });
2250
+ try {
2251
+ await Promise.race([client.connect(), timeoutPromise]);
2252
+ } finally {
2253
+ clearTimeout(timeoutTimer);
2254
+ }
2255
+ this.clients = this.clients.filter((c) => c.getSessionId() !== session.sessionId);
1993
2256
  this.clients.push(client);
1994
2257
  return;
1995
2258
  } catch (error) {
@@ -2001,36 +2264,32 @@ var MultiSessionClient = class {
2001
2264
  }
2002
2265
  console.error(`[MultiSessionClient] Failed to connect to session ${session.sessionId} after ${maxRetries + 1} attempts:`, lastError);
2003
2266
  }
2004
- async createAndConnectClient(session) {
2005
- const client = new MCPClient({
2006
- identity: this.identity,
2007
- sessionId: session.sessionId,
2008
- serverId: session.serverId,
2009
- serverUrl: session.serverUrl,
2010
- callbackUrl: session.callbackUrl,
2011
- serverName: session.serverName,
2012
- transportType: session.transportType,
2013
- headers: session.headers
2014
- });
2015
- const timeoutMs = this.options.timeout ?? 15e3;
2016
- const timeoutPromise = new Promise((_, reject) => {
2017
- setTimeout(() => reject(new Error(`Connection timed out after ${timeoutMs}ms`)), timeoutMs);
2018
- });
2019
- await Promise.race([client.connect(), timeoutPromise]);
2020
- return client;
2021
- }
2267
+ /**
2268
+ * The main entry point. Fetches all active sessions for this identity from
2269
+ * storage and establishes connections to all of them in batches.
2270
+ *
2271
+ * Call this once after creating the client. On traditional servers, you can
2272
+ * cache the `MultiSessionClient` instance after calling `connect()` to avoid
2273
+ * re-fetching and re-connecting on every request.
2274
+ */
2022
2275
  async connect() {
2023
2276
  const sessions = await this.getActiveSessions();
2024
2277
  await this.connectInBatches(sessions);
2025
2278
  }
2026
2279
  /**
2027
- * Returns the array of currently connected clients.
2280
+ * Returns all currently connected `MCPClient` instances.
2281
+ *
2282
+ * Use this to enumerate available tools across all connected servers,
2283
+ * or to route a tool call to the right client by `serverId`.
2028
2284
  */
2029
2285
  getClients() {
2030
2286
  return this.clients;
2031
2287
  }
2032
2288
  /**
2033
- * Disconnects all clients.
2289
+ * Gracefully disconnects all active MCP clients and clears the internal client list.
2290
+ *
2291
+ * Call this during server shutdown or when a user logs out to free up
2292
+ * underlying transport resources (SSE streams, HTTP connections, etc.).
2034
2293
  */
2035
2294
  disconnect() {
2036
2295
  this.clients.forEach((client) => client.disconnect());