@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
package/dist/index.mjs CHANGED
@@ -126,8 +126,6 @@ var SOFTWARE_ID = "@mcp-ts";
126
126
  var SOFTWARE_VERSION = "1.3.4";
127
127
  var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
128
128
  var MCP_CLIENT_VERSION = "2.0";
129
-
130
- // src/server/storage/redis-backend.ts
131
129
  var firstChar = customAlphabet(
132
130
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
133
131
  1
@@ -136,6 +134,18 @@ var rest = customAlphabet(
136
134
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
137
135
  11
138
136
  );
137
+ function sanitizeServerLabel(name) {
138
+ let sanitized = name.replace(/[^a-zA-Z0-9-_]/g, "_").replace(/_{2,}/g, "_").toLowerCase();
139
+ if (!/^[a-zA-Z]/.test(sanitized)) {
140
+ sanitized = "s_" + sanitized;
141
+ }
142
+ return sanitized;
143
+ }
144
+ function generateSessionId() {
145
+ return firstChar() + rest();
146
+ }
147
+
148
+ // src/server/storage/redis-backend.ts
139
149
  var RedisStorageBackend = class {
140
150
  constructor(redis2) {
141
151
  this.redis = redis2;
@@ -144,6 +154,14 @@ var RedisStorageBackend = class {
144
154
  __publicField(this, "IDENTITY_KEY_PREFIX", "mcp:identity:");
145
155
  __publicField(this, "IDENTITY_KEY_SUFFIX", ":sessions");
146
156
  }
157
+ async init() {
158
+ try {
159
+ await this.redis.ping();
160
+ console.log("[mcp-ts][Storage] Redis: \u2713 Connected to server.");
161
+ } catch (error) {
162
+ throw new Error(`[RedisStorage] Failed to connect to Redis: ${error.message}`);
163
+ }
164
+ }
147
165
  /**
148
166
  * Generates Redis key for a specific session
149
167
  * @private
@@ -186,7 +204,7 @@ var RedisStorageBackend = class {
186
204
  return Array.from(keys);
187
205
  }
188
206
  generateSessionId() {
189
- return firstChar() + rest();
207
+ return generateSessionId();
190
208
  }
191
209
  async createSession(session, ttl) {
192
210
  const { sessionId, identity } = session;
@@ -354,14 +372,8 @@ var RedisStorageBackend = class {
354
372
  }
355
373
  }
356
374
  };
357
- var firstChar2 = customAlphabet(
358
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
359
- 1
360
- );
361
- var rest2 = customAlphabet(
362
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
363
- 11
364
- );
375
+
376
+ // src/server/storage/memory-backend.ts
365
377
  var MemoryStorageBackend = class {
366
378
  constructor() {
367
379
  // Map<identity:sessionId, SessionData>
@@ -369,11 +381,14 @@ var MemoryStorageBackend = class {
369
381
  // Map<identity, Set<sessionId>>
370
382
  __publicField(this, "identitySessions", /* @__PURE__ */ new Map());
371
383
  }
384
+ async init() {
385
+ console.log("[mcp-ts][Storage] Memory: \u2713 internal memory store active.");
386
+ }
372
387
  getSessionKey(identity, sessionId) {
373
388
  return `${identity}:${sessionId}`;
374
389
  }
375
390
  generateSessionId() {
376
- return firstChar2() + rest2();
391
+ return generateSessionId();
377
392
  }
378
393
  async createSession(session, ttl) {
379
394
  const { sessionId, identity } = session;
@@ -444,14 +459,6 @@ var MemoryStorageBackend = class {
444
459
  async disconnect() {
445
460
  }
446
461
  };
447
- var firstChar3 = customAlphabet(
448
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
449
- 1
450
- );
451
- var rest3 = customAlphabet(
452
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
453
- 11
454
- );
455
462
  var FileStorageBackend = class {
456
463
  /**
457
464
  * @param options.path Path to the JSON file storage (default: ./sessions.json)
@@ -488,6 +495,7 @@ var FileStorageBackend = class {
488
495
  }
489
496
  }
490
497
  this.initialized = true;
498
+ console.log(`[mcp-ts][Storage] File: \u2713 storage directory at ${path.dirname(this.filePath)} verified.`);
491
499
  }
492
500
  async ensureInitialized() {
493
501
  if (!this.initialized) await this.init();
@@ -501,7 +509,7 @@ var FileStorageBackend = class {
501
509
  return `${identity}:${sessionId}`;
502
510
  }
503
511
  generateSessionId() {
504
- return firstChar3() + rest3();
512
+ return generateSessionId();
505
513
  }
506
514
  async createSession(session, ttl) {
507
515
  await this.ensureInitialized();
@@ -592,6 +600,7 @@ var SqliteStorage = class {
592
600
  CREATE INDEX IF NOT EXISTS idx_${this.table}_identity ON ${this.table}(identity);
593
601
  `);
594
602
  this.initialized = true;
603
+ console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
595
604
  } catch (error) {
596
605
  if (error.code === "MODULE_NOT_FOUND" || error.message?.includes("better-sqlite3")) {
597
606
  throw new Error(
@@ -607,12 +616,7 @@ var SqliteStorage = class {
607
616
  }
608
617
  }
609
618
  generateSessionId() {
610
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
611
- let result = "";
612
- for (let i = 0; i < 32; i++) {
613
- result += chars.charAt(Math.floor(Math.random() * chars.length));
614
- }
615
- return result;
619
+ return generateSessionId();
616
620
  }
617
621
  async createSession(session, ttl) {
618
622
  this.ensureInitialized();
@@ -707,6 +711,157 @@ var SqliteStorage = class {
707
711
  }
708
712
  };
709
713
 
714
+ // src/server/storage/supabase-backend.ts
715
+ var SupabaseStorageBackend = class {
716
+ constructor(supabase) {
717
+ this.supabase = supabase;
718
+ __publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
719
+ }
720
+ async init() {
721
+ const { error } = await this.supabase.from("mcp_sessions").select("session_id").limit(0);
722
+ if (error) {
723
+ if (error.code === "42P01") {
724
+ throw new Error(
725
+ '[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.'
726
+ );
727
+ }
728
+ throw new Error(`[SupabaseStorage] Initialization check failed: ${error.message}`);
729
+ }
730
+ console.log('[mcp-ts][Storage] Supabase: \u2713 "mcp_sessions" table verified.');
731
+ }
732
+ generateSessionId() {
733
+ return generateSessionId();
734
+ }
735
+ mapRowToSessionData(row) {
736
+ return {
737
+ sessionId: row.session_id,
738
+ serverId: row.server_id,
739
+ serverName: row.server_name,
740
+ serverUrl: row.server_url,
741
+ transportType: row.transport_type,
742
+ callbackUrl: row.callback_url,
743
+ createdAt: new Date(row.created_at).getTime(),
744
+ identity: row.identity,
745
+ headers: row.headers,
746
+ active: row.active,
747
+ clientInformation: row.client_information,
748
+ tokens: row.tokens,
749
+ codeVerifier: row.code_verifier,
750
+ clientId: row.client_id
751
+ };
752
+ }
753
+ async createSession(session, ttl) {
754
+ const { sessionId, identity } = session;
755
+ if (!sessionId || !identity) throw new Error("identity and sessionId required");
756
+ const effectiveTtl = ttl ?? this.DEFAULT_TTL;
757
+ const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
758
+ const { error } = await this.supabase.from("mcp_sessions").insert({
759
+ session_id: sessionId,
760
+ user_id: identity,
761
+ // Maps user_id to identity to support RLS using auth.uid()
762
+ server_id: session.serverId,
763
+ server_name: session.serverName,
764
+ server_url: session.serverUrl,
765
+ transport_type: session.transportType,
766
+ callback_url: session.callbackUrl,
767
+ created_at: new Date(session.createdAt || Date.now()).toISOString(),
768
+ identity,
769
+ headers: session.headers,
770
+ active: session.active ?? false,
771
+ client_information: session.clientInformation,
772
+ tokens: session.tokens,
773
+ code_verifier: session.codeVerifier,
774
+ client_id: session.clientId,
775
+ expires_at: expiresAt
776
+ });
777
+ if (error) {
778
+ if (error.code === "23505") {
779
+ throw new Error(`Session ${sessionId} already exists`);
780
+ }
781
+ throw new Error(`Failed to create session in Supabase: ${error.message}`);
782
+ }
783
+ }
784
+ async updateSession(identity, sessionId, data, ttl) {
785
+ const effectiveTtl = ttl ?? this.DEFAULT_TTL;
786
+ const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
787
+ const updateData = {
788
+ expires_at: expiresAt,
789
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
790
+ };
791
+ if ("serverId" in data) updateData.server_id = data.serverId;
792
+ if ("serverName" in data) updateData.server_name = data.serverName;
793
+ if ("serverUrl" in data) updateData.server_url = data.serverUrl;
794
+ if ("transportType" in data) updateData.transport_type = data.transportType;
795
+ if ("callbackUrl" in data) updateData.callback_url = data.callbackUrl;
796
+ if ("active" in data) updateData.active = data.active;
797
+ if ("headers" in data) updateData.headers = data.headers;
798
+ if ("clientInformation" in data) updateData.client_information = data.clientInformation;
799
+ if ("tokens" in data) updateData.tokens = data.tokens;
800
+ if ("codeVerifier" in data) updateData.code_verifier = data.codeVerifier;
801
+ if ("clientId" in data) updateData.client_id = data.clientId;
802
+ const { data: updatedRows, error } = await this.supabase.from("mcp_sessions").update(updateData).eq("identity", identity).eq("session_id", sessionId).select("id");
803
+ if (error) {
804
+ throw new Error(`Failed to update session: ${error.message}`);
805
+ }
806
+ if (!updatedRows || updatedRows.length === 0) {
807
+ throw new Error(`Session ${sessionId} not found for identity ${identity}`);
808
+ }
809
+ }
810
+ async getSession(identity, sessionId) {
811
+ const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("identity", identity).eq("session_id", sessionId).maybeSingle();
812
+ if (error) {
813
+ console.error("[SupabaseStorage] Failed to get session:", error);
814
+ return null;
815
+ }
816
+ if (!data) return null;
817
+ return this.mapRowToSessionData(data);
818
+ }
819
+ async getIdentitySessionsData(identity) {
820
+ const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("identity", identity);
821
+ if (error) {
822
+ console.error(`[SupabaseStorage] Failed to get session data for ${identity}:`, error);
823
+ return [];
824
+ }
825
+ return data.map((row) => this.mapRowToSessionData(row));
826
+ }
827
+ async removeSession(identity, sessionId) {
828
+ const { error } = await this.supabase.from("mcp_sessions").delete().eq("identity", identity).eq("session_id", sessionId);
829
+ if (error) {
830
+ console.error("[SupabaseStorage] Failed to remove session:", error);
831
+ }
832
+ }
833
+ async getIdentityMcpSessions(identity) {
834
+ const { data, error } = await this.supabase.from("mcp_sessions").select("session_id").eq("identity", identity);
835
+ if (error) {
836
+ console.error(`[SupabaseStorage] Failed to get sessions for ${identity}:`, error);
837
+ return [];
838
+ }
839
+ return data.map((row) => row.session_id);
840
+ }
841
+ async getAllSessionIds() {
842
+ const { data, error } = await this.supabase.from("mcp_sessions").select("session_id");
843
+ if (error) {
844
+ console.error("[SupabaseStorage] Failed to get all sessions:", error);
845
+ return [];
846
+ }
847
+ return data.map((row) => row.session_id);
848
+ }
849
+ async clearAll() {
850
+ const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
851
+ if (error) {
852
+ console.error("[SupabaseStorage] Failed to clear sessions:", error);
853
+ }
854
+ }
855
+ async cleanupExpiredSessions() {
856
+ const { error } = await this.supabase.from("mcp_sessions").delete().lt("expires_at", (/* @__PURE__ */ new Date()).toISOString());
857
+ if (error) {
858
+ console.error("[SupabaseStorage] Failed to cleanup expired sessions:", error);
859
+ }
860
+ }
861
+ async disconnect() {
862
+ }
863
+ };
864
+
710
865
  // src/server/storage/index.ts
711
866
  var storageInstance = null;
712
867
  var storagePromise = null;
@@ -725,53 +880,85 @@ async function createStorage() {
725
880
  try {
726
881
  const { getRedis: getRedis2 } = await Promise.resolve().then(() => (init_redis(), redis_exports));
727
882
  const redis2 = await getRedis2();
728
- console.log("[Storage] Using Redis storage (Explicit)");
729
- return new RedisStorageBackend(redis2);
883
+ console.log('[mcp-ts][Storage] Explicit selection: "redis"');
884
+ return await initializeStorage(new RedisStorageBackend(redis2));
730
885
  } catch (error) {
731
- console.error("[Storage] Failed to initialize Redis:", error.message);
732
- console.log("[Storage] Falling back to In-Memory storage");
733
- return new MemoryStorageBackend();
886
+ console.error("[mcp-ts][Storage] Failed to initialize Redis:", error.message);
887
+ console.log("[mcp-ts][Storage] Falling back to In-Memory storage");
888
+ return await initializeStorage(new MemoryStorageBackend());
734
889
  }
735
890
  }
736
891
  if (type === "file") {
737
892
  const filePath = process.env.MCP_TS_STORAGE_FILE;
738
- if (!filePath) {
739
- console.warn('[Storage] MCP_TS_STORAGE_TYPE is "file" but MCP_TS_STORAGE_FILE is missing');
740
- }
741
- console.log(`[Storage] Using File storage (${filePath}) (Explicit)`);
893
+ console.log(`[mcp-ts][Storage] Explicit selection: "file" (${filePath || "default"})`);
742
894
  return await initializeStorage(new FileStorageBackend({ path: filePath }));
743
895
  }
744
896
  if (type === "sqlite") {
745
897
  const dbPath = process.env.MCP_TS_STORAGE_SQLITE_PATH;
746
- console.log(`[Storage] Using SQLite storage (${dbPath || "default"}) (Explicit)`);
898
+ console.log(`[mcp-ts][Storage] Explicit selection: "sqlite" (${dbPath || "default"})`);
747
899
  return await initializeStorage(new SqliteStorage({ path: dbPath }));
748
900
  }
901
+ if (type === "supabase") {
902
+ const url = process.env.SUPABASE_URL;
903
+ const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_ANON_KEY;
904
+ if (!url || !key) {
905
+ console.warn('[mcp-ts][Storage] Explicit selection "supabase" requires SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY.');
906
+ } else {
907
+ if (!process.env.SUPABASE_SERVICE_ROLE_KEY) {
908
+ 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.');
909
+ }
910
+ try {
911
+ const { createClient } = await import('@supabase/supabase-js');
912
+ const client = createClient(url, key);
913
+ console.log('[mcp-ts][Storage] Explicit selection: "supabase"');
914
+ return await initializeStorage(new SupabaseStorageBackend(client));
915
+ } catch (error) {
916
+ console.error("[mcp-ts][Storage] Failed to initialize Supabase:", error.message);
917
+ console.log("[mcp-ts][Storage] Falling back to In-Memory storage");
918
+ return await initializeStorage(new MemoryStorageBackend());
919
+ }
920
+ }
921
+ }
749
922
  if (type === "memory") {
750
- console.log("[Storage] Using In-Memory storage (Explicit)");
751
- return new MemoryStorageBackend();
923
+ console.log('[mcp-ts][Storage] Explicit selection: "memory"');
924
+ return await initializeStorage(new MemoryStorageBackend());
752
925
  }
753
926
  if (process.env.REDIS_URL) {
754
927
  try {
755
928
  const { getRedis: getRedis2 } = await Promise.resolve().then(() => (init_redis(), redis_exports));
756
929
  const redis2 = await getRedis2();
757
- console.log("[Storage] Auto-detected REDIS_URL. Using Redis storage.");
758
- return new RedisStorageBackend(redis2);
930
+ console.log('[mcp-ts][Storage] Auto-detection: "redis" (via REDIS_URL)');
931
+ return await initializeStorage(new RedisStorageBackend(redis2));
759
932
  } catch (error) {
760
- console.error("[Storage] Redis auto-detection failed:", error.message);
761
- console.log("[Storage] Falling back to In-Memory storage");
762
- return new MemoryStorageBackend();
933
+ console.error("[mcp-ts][Storage] Redis auto-detection failed:", error.message);
934
+ console.log("[mcp-ts][Storage] Falling back to next available backend");
763
935
  }
764
936
  }
765
937
  if (process.env.MCP_TS_STORAGE_FILE) {
766
- console.log(`[Storage] Auto-detected MCP_TS_STORAGE_FILE. Using File storage (${process.env.MCP_TS_STORAGE_FILE}).`);
938
+ console.log(`[mcp-ts][Storage] Auto-detection: "file" (${process.env.MCP_TS_STORAGE_FILE})`);
767
939
  return await initializeStorage(new FileStorageBackend({ path: process.env.MCP_TS_STORAGE_FILE }));
768
940
  }
769
941
  if (process.env.MCP_TS_STORAGE_SQLITE_PATH) {
770
- console.log(`[Storage] Auto-detected MCP_TS_STORAGE_SQLITE_PATH. Using SQLite storage (${process.env.MCP_TS_STORAGE_SQLITE_PATH}).`);
942
+ console.log(`[mcp-ts][Storage] Auto-detection: "sqlite" (${process.env.MCP_TS_STORAGE_SQLITE_PATH})`);
771
943
  return await initializeStorage(new SqliteStorage({ path: process.env.MCP_TS_STORAGE_SQLITE_PATH }));
772
944
  }
773
- console.log("[Storage] No storage configured. Using In-Memory storage (Default).");
774
- return new MemoryStorageBackend();
945
+ if (process.env.SUPABASE_URL && (process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_ANON_KEY)) {
946
+ try {
947
+ const { createClient } = await import('@supabase/supabase-js');
948
+ const url = process.env.SUPABASE_URL;
949
+ const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_ANON_KEY;
950
+ if (!process.env.SUPABASE_SERVICE_ROLE_KEY) {
951
+ 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.');
952
+ }
953
+ const client = createClient(url, key);
954
+ console.log('[mcp-ts][Storage] Auto-detection: "supabase" (via SUPABASE_URL)');
955
+ return await initializeStorage(new SupabaseStorageBackend(client));
956
+ } catch (error) {
957
+ console.error("[mcp-ts][Storage] Supabase auto-detection failed:", error.message);
958
+ }
959
+ }
960
+ console.log('[mcp-ts][Storage] Defaulting to: "memory"');
961
+ return await initializeStorage(new MemoryStorageBackend());
775
962
  }
776
963
  async function getStorage() {
777
964
  if (storageInstance) {
@@ -981,15 +1168,6 @@ var StorageOAuthClientProvider = class {
981
1168
  }
982
1169
  };
983
1170
 
984
- // src/shared/utils.ts
985
- function sanitizeServerLabel(name) {
986
- let sanitized = name.replace(/[^a-zA-Z0-9-_]/g, "_").replace(/_{2,}/g, "_").toLowerCase();
987
- if (!/^[a-zA-Z]/.test(sanitized)) {
988
- sanitized = "s_" + sanitized;
989
- }
990
- return sanitized;
991
- }
992
-
993
1171
  // src/shared/events.ts
994
1172
  var Emitter = class {
995
1173
  constructor() {
@@ -2020,47 +2198,132 @@ var MCPClient = class _MCPClient {
2020
2198
  };
2021
2199
 
2022
2200
  // src/server/mcp/multi-session-client.ts
2201
+ var DEFAULT_TIMEOUT_MS = 15e3;
2202
+ var DEFAULT_MAX_RETRIES = 2;
2203
+ var DEFAULT_RETRY_DELAY_MS = 1e3;
2204
+ var CONNECTION_BATCH_SIZE = 5;
2023
2205
  var MultiSessionClient = class {
2206
+ /**
2207
+ * Creates a new MultiSessionClient for the given user identity.
2208
+ *
2209
+ * @param identity - A unique string identifying the user (e.g. user ID or email).
2210
+ * @param options - Optional tuning for connection timeout, retry count, and retry delay.
2211
+ * Falls back to sensible defaults if not provided.
2212
+ */
2024
2213
  constructor(identity, options = {}) {
2025
2214
  __publicField(this, "clients", []);
2026
2215
  __publicField(this, "identity");
2027
2216
  __publicField(this, "options");
2217
+ __publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
2028
2218
  this.identity = identity;
2029
2219
  this.options = {
2030
- timeout: 15e3,
2031
- maxRetries: 2,
2032
- retryDelay: 1e3,
2220
+ timeout: DEFAULT_TIMEOUT_MS,
2221
+ maxRetries: DEFAULT_MAX_RETRIES,
2222
+ retryDelay: DEFAULT_RETRY_DELAY_MS,
2033
2223
  ...options
2034
2224
  };
2035
2225
  }
2226
+ /**
2227
+ * Fetches all sessions for this identity from storage and returns only the
2228
+ * ones that are ready to connect.
2229
+ *
2230
+ * A session is considered connectable when:
2231
+ * - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
2232
+ * - Its `active` flag is not explicitly `false` — sessions with `active: false` are
2233
+ * either mid-OAuth flow, auth-pending, or previously failed. We skip those here
2234
+ * and let the OAuth flow complete separately before we try to reconnect them.
2235
+ *
2236
+ * Note: Sessions where `active` is `undefined` (legacy records) are included
2237
+ * for backwards compatibility.
2238
+ */
2036
2239
  async getActiveSessions() {
2037
2240
  const sessions = await storage.getIdentitySessionsData(this.identity);
2038
- console.log(
2039
- `[MultiSessionClient] All sessions for ${this.identity}:`,
2040
- sessions.map((s) => ({ sessionId: s.sessionId, serverId: s.serverId }))
2241
+ const valid = sessions.filter(
2242
+ (s) => s.serverId && s.serverUrl && s.callbackUrl && s.active !== false
2243
+ // exclude OAuth-pending / failed sessions
2041
2244
  );
2042
- const valid = sessions.filter((s) => s.serverId && s.serverUrl && s.callbackUrl);
2043
- console.log(`[MultiSessionClient] Filtered valid sessions:`, valid.length);
2044
2245
  return valid;
2045
2246
  }
2247
+ /**
2248
+ * Connects to a list of sessions in controlled batches of `CONNECTION_BATCH_SIZE`.
2249
+ *
2250
+ * Batching prevents overwhelming the event loop or external servers when a user
2251
+ * has many active MCP sessions (e.g. 20+ servers). Within each batch, sessions
2252
+ * are connected concurrently using `Promise.all` for speed.
2253
+ */
2046
2254
  async connectInBatches(sessions) {
2047
- const BATCH_SIZE = 5;
2048
- for (let i = 0; i < sessions.length; i += BATCH_SIZE) {
2049
- const batch = sessions.slice(i, i + BATCH_SIZE);
2255
+ for (let i = 0; i < sessions.length; i += CONNECTION_BATCH_SIZE) {
2256
+ const batch = sessions.slice(i, i + CONNECTION_BATCH_SIZE);
2050
2257
  await Promise.all(batch.map((session) => this.connectSession(session)));
2051
2258
  }
2052
2259
  }
2260
+ /**
2261
+ * Connects a single session, with built-in deduplication to prevent race conditions.
2262
+ *
2263
+ * - If a client for this session already exists and is connected, returns immediately.
2264
+ * - If a connection attempt for this session is already in-flight (e.g. from a
2265
+ * concurrent call), it joins the existing promise instead of starting a new one.
2266
+ * This is the key concurrency lock — the `connectionPromises` map acts as a
2267
+ * per-session mutex so we never spin up two physical connections for the same session.
2268
+ * - On completion (success or failure), the promise is cleaned up from the map.
2269
+ */
2053
2270
  async connectSession(session) {
2054
2271
  const existingClient = this.clients.find((c) => c.getSessionId() === session.sessionId);
2055
2272
  if (existingClient?.isConnected()) {
2056
2273
  return;
2057
2274
  }
2058
- const maxRetries = this.options.maxRetries ?? 2;
2059
- const retryDelay = this.options.retryDelay ?? 1e3;
2275
+ if (this.connectionPromises.has(session.sessionId)) {
2276
+ return this.connectionPromises.get(session.sessionId);
2277
+ }
2278
+ const connectPromise = this.establishConnectionWithRetries(session);
2279
+ this.connectionPromises.set(session.sessionId, connectPromise);
2280
+ try {
2281
+ await connectPromise;
2282
+ } finally {
2283
+ this.connectionPromises.delete(session.sessionId);
2284
+ }
2285
+ }
2286
+ /**
2287
+ * The core connection loop for a single session.
2288
+ *
2289
+ * Attempts to establish a physical MCP connection, retrying up to `maxRetries` times
2290
+ * if the connection fails. Each attempt:
2291
+ * 1. Creates a fresh `MCPClient` instance from the session data.
2292
+ * 2. Races the connect call against a timeout promise — if the server doesn't respond
2293
+ * within `timeoutMs`, the attempt is aborted and counted as a failure.
2294
+ * 3. On success, replaces any stale client entry for this session in the `clients` array.
2295
+ * 4. On failure, waits `retryDelay` ms before the next attempt.
2296
+ *
2297
+ * If all attempts are exhausted, logs an error and returns silently (does not throw),
2298
+ * so a single bad server doesn't block the rest of the batch from connecting.
2299
+ */
2300
+ async establishConnectionWithRetries(session) {
2301
+ const maxRetries = this.options.maxRetries ?? DEFAULT_MAX_RETRIES;
2302
+ const retryDelay = this.options.retryDelay ?? DEFAULT_RETRY_DELAY_MS;
2060
2303
  let lastError;
2061
2304
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
2062
2305
  try {
2063
- const client = await this.createAndConnectClient(session);
2306
+ const client = new MCPClient({
2307
+ identity: this.identity,
2308
+ sessionId: session.sessionId,
2309
+ serverId: session.serverId,
2310
+ serverUrl: session.serverUrl,
2311
+ callbackUrl: session.callbackUrl,
2312
+ serverName: session.serverName,
2313
+ transportType: session.transportType,
2314
+ headers: session.headers
2315
+ });
2316
+ const timeoutMs = this.options.timeout ?? DEFAULT_TIMEOUT_MS;
2317
+ let timeoutTimer;
2318
+ const timeoutPromise = new Promise((_, reject) => {
2319
+ timeoutTimer = setTimeout(() => reject(new Error(`Connection timed out after ${timeoutMs}ms`)), timeoutMs);
2320
+ });
2321
+ try {
2322
+ await Promise.race([client.connect(), timeoutPromise]);
2323
+ } finally {
2324
+ clearTimeout(timeoutTimer);
2325
+ }
2326
+ this.clients = this.clients.filter((c) => c.getSessionId() !== session.sessionId);
2064
2327
  this.clients.push(client);
2065
2328
  return;
2066
2329
  } catch (error) {
@@ -2072,36 +2335,32 @@ var MultiSessionClient = class {
2072
2335
  }
2073
2336
  console.error(`[MultiSessionClient] Failed to connect to session ${session.sessionId} after ${maxRetries + 1} attempts:`, lastError);
2074
2337
  }
2075
- async createAndConnectClient(session) {
2076
- const client = new MCPClient({
2077
- identity: this.identity,
2078
- sessionId: session.sessionId,
2079
- serverId: session.serverId,
2080
- serverUrl: session.serverUrl,
2081
- callbackUrl: session.callbackUrl,
2082
- serverName: session.serverName,
2083
- transportType: session.transportType,
2084
- headers: session.headers
2085
- });
2086
- const timeoutMs = this.options.timeout ?? 15e3;
2087
- const timeoutPromise = new Promise((_, reject) => {
2088
- setTimeout(() => reject(new Error(`Connection timed out after ${timeoutMs}ms`)), timeoutMs);
2089
- });
2090
- await Promise.race([client.connect(), timeoutPromise]);
2091
- return client;
2092
- }
2338
+ /**
2339
+ * The main entry point. Fetches all active sessions for this identity from
2340
+ * storage and establishes connections to all of them in batches.
2341
+ *
2342
+ * Call this once after creating the client. On traditional servers, you can
2343
+ * cache the `MultiSessionClient` instance after calling `connect()` to avoid
2344
+ * re-fetching and re-connecting on every request.
2345
+ */
2093
2346
  async connect() {
2094
2347
  const sessions = await this.getActiveSessions();
2095
2348
  await this.connectInBatches(sessions);
2096
2349
  }
2097
2350
  /**
2098
- * Returns the array of currently connected clients.
2351
+ * Returns all currently connected `MCPClient` instances.
2352
+ *
2353
+ * Use this to enumerate available tools across all connected servers,
2354
+ * or to route a tool call to the right client by `serverId`.
2099
2355
  */
2100
2356
  getClients() {
2101
2357
  return this.clients;
2102
2358
  }
2103
2359
  /**
2104
- * Disconnects all clients.
2360
+ * Gracefully disconnects all active MCP clients and clears the internal client list.
2361
+ *
2362
+ * Call this during server shutdown or when a user logs out to free up
2363
+ * underlying transport resources (SSE streams, HTTP connections, etc.).
2105
2364
  */
2106
2365
  disconnect() {
2107
2366
  this.clients.forEach((client) => client.disconnect());