@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.
- package/dist/adapters/agui-adapter.d.mts +1 -1
- package/dist/adapters/agui-adapter.d.ts +1 -1
- package/dist/adapters/agui-adapter.js +2 -2
- package/dist/adapters/agui-adapter.js.map +1 -1
- package/dist/adapters/agui-adapter.mjs +2 -2
- package/dist/adapters/agui-adapter.mjs.map +1 -1
- package/dist/adapters/agui-middleware.d.mts +1 -1
- package/dist/adapters/agui-middleware.d.ts +1 -1
- package/dist/adapters/agui-middleware.js.map +1 -1
- package/dist/adapters/agui-middleware.mjs.map +1 -1
- package/dist/adapters/ai-adapter.d.mts +1 -1
- package/dist/adapters/ai-adapter.d.ts +1 -1
- package/dist/adapters/ai-adapter.js +1 -1
- package/dist/adapters/ai-adapter.js.map +1 -1
- package/dist/adapters/ai-adapter.mjs +1 -1
- package/dist/adapters/ai-adapter.mjs.map +1 -1
- package/dist/adapters/langchain-adapter.d.mts +1 -1
- package/dist/adapters/langchain-adapter.d.ts +1 -1
- package/dist/adapters/langchain-adapter.js +1 -1
- package/dist/adapters/langchain-adapter.js.map +1 -1
- package/dist/adapters/langchain-adapter.mjs +1 -1
- package/dist/adapters/langchain-adapter.mjs.map +1 -1
- package/dist/adapters/mastra-adapter.d.mts +1 -1
- package/dist/adapters/mastra-adapter.d.ts +1 -1
- package/dist/adapters/mastra-adapter.js +1 -1
- package/dist/adapters/mastra-adapter.js.map +1 -1
- package/dist/adapters/mastra-adapter.mjs +1 -1
- package/dist/adapters/mastra-adapter.mjs.map +1 -1
- package/dist/bin/mcp-ts.d.mts +1 -0
- package/dist/bin/mcp-ts.d.ts +1 -0
- package/dist/bin/mcp-ts.js +105 -0
- package/dist/bin/mcp-ts.js.map +1 -0
- package/dist/bin/mcp-ts.mjs +82 -0
- package/dist/bin/mcp-ts.mjs.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +411 -90
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +350 -91
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-BYLarghq.d.ts → multi-session-client-CHE8QpVE.d.ts} +75 -5
- package/dist/{multi-session-client-CzhMkE0k.d.mts → multi-session-client-CQsRbxYI.d.mts} +75 -5
- package/dist/server/index.d.mts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +394 -90
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +350 -91
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.js +10 -2
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +10 -2
- package/dist/shared/index.mjs.map +1 -1
- package/package.json +19 -6
- package/src/adapters/agui-adapter.ts +222 -222
- package/src/adapters/ai-adapter.ts +115 -115
- package/src/adapters/langchain-adapter.ts +127 -127
- package/src/adapters/mastra-adapter.ts +126 -126
- package/src/bin/mcp-ts.ts +102 -0
- package/src/server/handlers/nextjs-handler.ts +12 -12
- package/src/server/handlers/sse-handler.ts +61 -61
- package/src/server/mcp/multi-session-client.ts +135 -39
- package/src/server/storage/file-backend.ts +4 -16
- package/src/server/storage/index.ts +68 -25
- package/src/server/storage/memory-backend.ts +7 -16
- package/src/server/storage/redis-backend.ts +12 -16
- package/src/server/storage/sqlite-backend.ts +3 -6
- package/src/server/storage/supabase-backend.ts +228 -0
- package/src/shared/event-routing.ts +28 -28
- package/src/shared/utils.ts +22 -0
- package/supabase/migrations/20260330195700_install_mcp_sessions.sql +84 -0
package/dist/server/index.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
355
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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]
|
|
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(
|
|
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(
|
|
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
|
|
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-
|
|
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-
|
|
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
|
-
|
|
771
|
-
|
|
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:
|
|
1960
|
-
maxRetries:
|
|
1961
|
-
retryDelay:
|
|
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
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
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
|
-
|
|
1977
|
-
|
|
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
|
-
|
|
1988
|
-
|
|
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 =
|
|
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
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
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
|
|
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
|
-
*
|
|
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());
|