@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/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
|
|
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
|
-
|
|
358
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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]
|
|
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(
|
|
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(
|
|
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
|
|
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-
|
|
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-
|
|
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
|
-
|
|
774
|
-
|
|
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:
|
|
2031
|
-
maxRetries:
|
|
2032
|
-
retryDelay:
|
|
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
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
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
|
-
|
|
2048
|
-
|
|
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
|
-
|
|
2059
|
-
|
|
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 =
|
|
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
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
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
|
|
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
|
-
*
|
|
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());
|