@mcp-ts/sdk 2.3.4 → 2.4.1
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/README.md +161 -39
- package/dist/adapters/agui-adapter.d.mts +4 -4
- package/dist/adapters/agui-adapter.d.ts +4 -4
- package/dist/adapters/agui-middleware.d.mts +4 -4
- package/dist/adapters/agui-middleware.d.ts +4 -4
- package/dist/adapters/ai-adapter.d.mts +4 -4
- package/dist/adapters/ai-adapter.d.ts +4 -4
- package/dist/adapters/langchain-adapter.d.mts +4 -4
- package/dist/adapters/langchain-adapter.d.ts +4 -4
- package/dist/adapters/mastra-adapter.d.mts +2 -2
- package/dist/adapters/mastra-adapter.d.ts +2 -2
- package/dist/client/index.d.mts +3 -3
- package/dist/client/index.d.ts +3 -3
- package/dist/client/index.js +2 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +2 -2
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +11 -10
- package/dist/client/react.d.ts +11 -10
- package/dist/client/react.js +66 -26
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +67 -27
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +8 -7
- package/dist/client/vue.d.ts +8 -7
- package/dist/client/vue.js +27 -17
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +27 -17
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{events-CK3N--3g.d.mts → events-C4m7tK1U.d.mts} +0 -1
- package/dist/{events-CK3N--3g.d.ts → events-C4m7tK1U.d.ts} +0 -1
- package/dist/{index-CmjMd2ac.d.ts → index-Ch7ouNSa.d.ts} +3 -3
- package/dist/{index-Cfjsme-a.d.mts → index-L5XoXgsb.d.mts} +3 -3
- package/dist/index.d.mts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +799 -326
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +793 -324
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-C_kPHpD5.d.ts → multi-session-client-Bwm-efqg.d.ts} +43 -38
- package/dist/{multi-session-client-C7hGqzgM.d.mts → multi-session-client-k-9RvWvi.d.mts} +43 -38
- package/dist/server/index.d.mts +48 -20
- package/dist/server/index.d.ts +48 -20
- package/dist/server/index.js +791 -320
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +787 -319
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +10 -9
- package/dist/shared/index.d.ts +10 -9
- package/dist/shared/index.js +7 -5
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +5 -4
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{tool-router-BMzhoNYt.d.ts → tool-router-CZMrOG8J.d.ts} +1 -1
- package/dist/{tool-router-BhHsvBfP.d.mts → tool-router-CuApsDiV.d.mts} +1 -1
- package/dist/{types-CxFaaZrM.d.mts → types-DCk_IF4L.d.mts} +5 -3
- package/dist/{types-CxFaaZrM.d.ts → types-DCk_IF4L.d.ts} +5 -3
- package/migrations/neon/20260513010000_install_mcp_sessions.sql +40 -3
- package/migrations/neon/20260513020000_add_session_cleanup_cron.sql +4 -4
- package/migrations/supabase/20260330195700_install_mcp_sessions.sql +67 -3
- package/migrations/supabase/20260421010000_add_session_cleanup_cron.sql +6 -6
- package/migrations/v2.3.4/neon/20260513010000_install_mcp_sessions.sql +69 -0
- package/migrations/v2.3.4/neon/20260513020000_add_session_cleanup_cron.sql +35 -0
- package/migrations/v2.3.4/supabase/20260330195700_install_mcp_sessions.sql +82 -0
- package/migrations/v2.3.4/supabase/20260421010000_add_session_cleanup_cron.sql +32 -0
- package/package.json +13 -3
- package/src/client/core/sse-client.ts +2 -2
- package/src/client/react/index.ts +1 -1
- package/src/client/react/oauth-popup.tsx +34 -13
- package/src/client/react/use-mcp.ts +19 -45
- package/src/client/utils/session-state.ts +43 -0
- package/src/client/vue/use-mcp.ts +18 -47
- package/src/server/handlers/sse-handler.ts +16 -9
- package/src/server/mcp/multi-session-client.ts +48 -11
- package/src/server/mcp/oauth-client.ts +133 -111
- package/src/server/mcp/storage-oauth-provider.ts +79 -50
- package/src/server/storage/file-backend.ts +74 -18
- package/src/server/storage/index.ts +2 -4
- package/src/server/storage/memory-backend.ts +49 -13
- package/src/server/storage/neon-backend.ts +201 -68
- package/src/server/storage/redis-backend.ts +81 -23
- package/src/server/storage/session-lifecycle.ts +78 -0
- package/src/server/storage/sqlite-backend.ts +89 -15
- package/src/server/storage/supabase-backend.ts +188 -63
- package/src/server/storage/types.ts +49 -16
- package/src/shared/constants.ts +5 -6
- package/src/shared/events.ts +0 -1
- package/src/shared/types.ts +5 -3
- package/src/shared/utils.ts +26 -0
package/dist/server/index.mjs
CHANGED
|
@@ -111,19 +111,7 @@ var init_redis = __esm({
|
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
113
|
});
|
|
114
|
-
|
|
115
|
-
// src/shared/constants.ts
|
|
116
|
-
var SESSION_TTL_SECONDS = 43200;
|
|
117
|
-
var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
|
|
118
|
-
var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
119
|
-
var DEFAULT_CLIENT_NAME = "MCP Assistant";
|
|
120
|
-
var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
|
|
121
|
-
var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
|
|
122
|
-
var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
|
|
123
|
-
var SOFTWARE_ID = "@mcp-ts";
|
|
124
|
-
var SOFTWARE_VERSION = "1.3.4";
|
|
125
|
-
var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
|
|
126
|
-
var MCP_CLIENT_VERSION = "2.0";
|
|
114
|
+
var OAUTH_STATE_SEPARATOR = ".";
|
|
127
115
|
var firstChar = customAlphabet(
|
|
128
116
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
|
129
117
|
1
|
|
@@ -142,13 +130,95 @@ function sanitizeServerLabel(name) {
|
|
|
142
130
|
function generateSessionId() {
|
|
143
131
|
return firstChar() + rest();
|
|
144
132
|
}
|
|
133
|
+
function formatOAuthState(nonce, sessionId) {
|
|
134
|
+
return `${nonce}${OAUTH_STATE_SEPARATOR}${sessionId}`;
|
|
135
|
+
}
|
|
136
|
+
function parseOAuthState(state) {
|
|
137
|
+
const separatorIndex = state.indexOf(OAUTH_STATE_SEPARATOR);
|
|
138
|
+
if (separatorIndex <= 0 || separatorIndex === state.length - 1) {
|
|
139
|
+
return void 0;
|
|
140
|
+
}
|
|
141
|
+
const nonce = state.slice(0, separatorIndex);
|
|
142
|
+
const sessionId = state.slice(separatorIndex + 1);
|
|
143
|
+
if (!nonce || !sessionId || sessionId.includes(OAUTH_STATE_SEPARATOR)) {
|
|
144
|
+
return void 0;
|
|
145
|
+
}
|
|
146
|
+
return { nonce, sessionId };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/shared/constants.ts
|
|
150
|
+
var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
|
|
151
|
+
var DORMANT_SESSION_EXPIRATION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
152
|
+
var DORMANT_SESSION_EXPIRATION_SECONDS = Math.floor(DORMANT_SESSION_EXPIRATION_MS / 1e3);
|
|
153
|
+
var DEFAULT_CLIENT_NAME = "MCP Assistant";
|
|
154
|
+
var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
|
|
155
|
+
var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
|
|
156
|
+
var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
|
|
157
|
+
var SOFTWARE_ID = "@mcp-ts";
|
|
158
|
+
var SOFTWARE_VERSION = "2.3.4";
|
|
159
|
+
var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
|
|
160
|
+
var MCP_CLIENT_VERSION = "2.0";
|
|
161
|
+
|
|
162
|
+
// src/server/storage/session-lifecycle.ts
|
|
163
|
+
function resolveSessionExpiresAt(status = "pending", referenceTime = Date.now()) {
|
|
164
|
+
return status === "active" ? null : referenceTime + STATE_EXPIRATION_MS;
|
|
165
|
+
}
|
|
166
|
+
function resolveSessionRedisTtlSeconds(session) {
|
|
167
|
+
return session.status === "active" ? DORMANT_SESSION_EXPIRATION_SECONDS : Math.floor(STATE_EXPIRATION_MS / 1e3);
|
|
168
|
+
}
|
|
169
|
+
function normalizeNewSession(session, now = Date.now()) {
|
|
170
|
+
const createdAt = session.createdAt || now;
|
|
171
|
+
const updatedAt = session.updatedAt ?? createdAt;
|
|
172
|
+
const status = session.status ?? "pending";
|
|
173
|
+
return {
|
|
174
|
+
...session,
|
|
175
|
+
status,
|
|
176
|
+
createdAt,
|
|
177
|
+
updatedAt,
|
|
178
|
+
expiresAt: resolveSessionExpiresAt(status, status === "active" ? updatedAt : createdAt)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function mergeSessionUpdate(current, data, now = Date.now()) {
|
|
182
|
+
const updatedAt = data.updatedAt ?? now;
|
|
183
|
+
const updated = {
|
|
184
|
+
...current,
|
|
185
|
+
...data,
|
|
186
|
+
updatedAt
|
|
187
|
+
};
|
|
188
|
+
const status = updated.status ?? "pending";
|
|
189
|
+
return {
|
|
190
|
+
...updated,
|
|
191
|
+
status,
|
|
192
|
+
expiresAt: resolveSessionExpiresAt(status, updatedAt)
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function normalizeStoredSession(session) {
|
|
196
|
+
const createdAt = session.createdAt || Date.now();
|
|
197
|
+
const updatedAt = session.updatedAt ?? createdAt;
|
|
198
|
+
const status = session.status ?? "pending";
|
|
199
|
+
const expiresAt = status === "active" ? null : session.expiresAt ?? resolveSessionExpiresAt(status, createdAt);
|
|
200
|
+
return {
|
|
201
|
+
...session,
|
|
202
|
+
status,
|
|
203
|
+
createdAt,
|
|
204
|
+
updatedAt,
|
|
205
|
+
expiresAt
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function isSessionExpired(session, now = Date.now()) {
|
|
209
|
+
const hydrated = normalizeStoredSession(session);
|
|
210
|
+
if (hydrated.status === "active") {
|
|
211
|
+
return hydrated.updatedAt !== void 0 && hydrated.updatedAt < now - DORMANT_SESSION_EXPIRATION_MS;
|
|
212
|
+
}
|
|
213
|
+
return typeof hydrated.expiresAt === "number" && hydrated.expiresAt < now;
|
|
214
|
+
}
|
|
145
215
|
|
|
146
216
|
// src/server/storage/redis-backend.ts
|
|
147
217
|
var RedisStorageBackend = class {
|
|
148
218
|
constructor(redis2) {
|
|
149
219
|
this.redis = redis2;
|
|
150
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
151
220
|
__publicField(this, "KEY_PREFIX", "mcp:session:");
|
|
221
|
+
__publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
|
|
152
222
|
__publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
|
|
153
223
|
__publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
|
|
154
224
|
}
|
|
@@ -167,6 +237,9 @@ var RedisStorageBackend = class {
|
|
|
167
237
|
getSessionKey(userId, sessionId) {
|
|
168
238
|
return `${this.KEY_PREFIX}${userId}:${sessionId}`;
|
|
169
239
|
}
|
|
240
|
+
getCredentialsKey(userId, sessionId) {
|
|
241
|
+
return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
|
|
242
|
+
}
|
|
170
243
|
/**
|
|
171
244
|
* Generates Redis key for tracking all sessions for a user
|
|
172
245
|
* @private
|
|
@@ -204,15 +277,16 @@ var RedisStorageBackend = class {
|
|
|
204
277
|
generateSessionId() {
|
|
205
278
|
return generateSessionId();
|
|
206
279
|
}
|
|
207
|
-
async create(session
|
|
280
|
+
async create(session) {
|
|
208
281
|
const { sessionId, userId } = session;
|
|
209
282
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
210
283
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
211
284
|
const userIdKey = this.getUserIdKey(userId);
|
|
212
|
-
const
|
|
285
|
+
const sessionWithLifecycle = normalizeNewSession(session);
|
|
286
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(sessionWithLifecycle);
|
|
213
287
|
const result = await this.redis.set(
|
|
214
288
|
sessionKey,
|
|
215
|
-
JSON.stringify(
|
|
289
|
+
JSON.stringify(sessionWithLifecycle),
|
|
216
290
|
"EX",
|
|
217
291
|
effectiveTtl,
|
|
218
292
|
"NX"
|
|
@@ -222,9 +296,8 @@ var RedisStorageBackend = class {
|
|
|
222
296
|
}
|
|
223
297
|
await this.redis.sadd(userIdKey, sessionId);
|
|
224
298
|
}
|
|
225
|
-
async update(userId, sessionId, data
|
|
299
|
+
async update(userId, sessionId, data) {
|
|
226
300
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
227
|
-
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
228
301
|
const script = `
|
|
229
302
|
local currentStr = redis.call("GET", KEYS[1])
|
|
230
303
|
if not currentStr then
|
|
@@ -232,26 +305,47 @@ var RedisStorageBackend = class {
|
|
|
232
305
|
end
|
|
233
306
|
|
|
234
307
|
local current = cjson.decode(currentStr)
|
|
235
|
-
local
|
|
236
|
-
|
|
237
|
-
for k,v in pairs(updates) do
|
|
238
|
-
current[k] = v
|
|
239
|
-
end
|
|
308
|
+
local updated = cjson.decode(ARGV[1])
|
|
240
309
|
|
|
241
|
-
redis.call("SET", KEYS[1], cjson.encode(
|
|
310
|
+
redis.call("SET", KEYS[1], cjson.encode(updated), "EX", ARGV[2])
|
|
242
311
|
return 1
|
|
243
312
|
`;
|
|
313
|
+
const current = await this.get(userId, sessionId);
|
|
314
|
+
if (!current) {
|
|
315
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
316
|
+
}
|
|
317
|
+
const updated = mergeSessionUpdate(current, data);
|
|
318
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
|
|
244
319
|
const result = await this.redis.eval(
|
|
245
320
|
script,
|
|
246
321
|
1,
|
|
247
322
|
sessionKey,
|
|
248
|
-
JSON.stringify(
|
|
323
|
+
JSON.stringify(updated),
|
|
249
324
|
effectiveTtl
|
|
250
325
|
);
|
|
251
326
|
if (result === 0) {
|
|
252
327
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
253
328
|
}
|
|
254
329
|
}
|
|
330
|
+
async patchCredentials(userId, sessionId, data) {
|
|
331
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
332
|
+
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
333
|
+
const sessionExists = await this.redis.exists(sessionKey);
|
|
334
|
+
if (!sessionExists) {
|
|
335
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
336
|
+
}
|
|
337
|
+
const session = await this.get(userId, sessionId);
|
|
338
|
+
const currentTtl = await this.redis.ttl(sessionKey);
|
|
339
|
+
const effectiveTtl = currentTtl > 0 && session ? currentTtl : resolveSessionRedisTtlSeconds(session ?? { status: "pending" });
|
|
340
|
+
const currentStr = await this.redis.get(credentialsKey);
|
|
341
|
+
const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
|
|
342
|
+
const credentials = { ...current, ...data, sessionId, userId };
|
|
343
|
+
if (effectiveTtl > 0) {
|
|
344
|
+
await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
|
|
345
|
+
} else {
|
|
346
|
+
await this.redis.set(credentialsKey, JSON.stringify(credentials));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
255
349
|
async get(userId, sessionId) {
|
|
256
350
|
try {
|
|
257
351
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
@@ -259,13 +353,28 @@ var RedisStorageBackend = class {
|
|
|
259
353
|
if (!sessionDataStr) {
|
|
260
354
|
return null;
|
|
261
355
|
}
|
|
262
|
-
const
|
|
263
|
-
return
|
|
356
|
+
const session = JSON.parse(sessionDataStr);
|
|
357
|
+
return normalizeStoredSession(session);
|
|
264
358
|
} catch (error) {
|
|
265
359
|
console.error("[RedisStorageBackend] Failed to get session:", error);
|
|
266
360
|
return null;
|
|
267
361
|
}
|
|
268
362
|
}
|
|
363
|
+
async getCredentials(userId, sessionId) {
|
|
364
|
+
const session = await this.get(userId, sessionId);
|
|
365
|
+
if (!session) return null;
|
|
366
|
+
const credentialsStr = await this.redis.get(this.getCredentialsKey(userId, sessionId));
|
|
367
|
+
return credentialsStr ? JSON.parse(credentialsStr) : { sessionId, userId };
|
|
368
|
+
}
|
|
369
|
+
async clearCredentials(userId, sessionId) {
|
|
370
|
+
await this.patchCredentials(userId, sessionId, {
|
|
371
|
+
clientInformation: null,
|
|
372
|
+
tokens: null,
|
|
373
|
+
codeVerifier: null,
|
|
374
|
+
clientId: null,
|
|
375
|
+
oauthState: null
|
|
376
|
+
});
|
|
377
|
+
}
|
|
269
378
|
async listIds(userId) {
|
|
270
379
|
const sessions2 = await this.list(userId);
|
|
271
380
|
return sessions2.map((session) => session.sessionId);
|
|
@@ -285,7 +394,7 @@ var RedisStorageBackend = class {
|
|
|
285
394
|
if (staleSessionIds.length > 0) {
|
|
286
395
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
287
396
|
}
|
|
288
|
-
return results.filter((session) => session !== null);
|
|
397
|
+
return results.filter((session) => session !== null).map((session) => normalizeStoredSession(session));
|
|
289
398
|
} catch (error) {
|
|
290
399
|
console.error(`[RedisStorageBackend] Failed to get session data for ${userId}:`, error);
|
|
291
400
|
return [];
|
|
@@ -294,9 +403,10 @@ var RedisStorageBackend = class {
|
|
|
294
403
|
async delete(userId, sessionId) {
|
|
295
404
|
try {
|
|
296
405
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
406
|
+
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
297
407
|
const userIdKey = this.getUserIdKey(userId);
|
|
298
408
|
await this.redis.srem(userIdKey, sessionId);
|
|
299
|
-
await this.redis.del(sessionKey);
|
|
409
|
+
await this.redis.del(sessionKey, credentialsKey);
|
|
300
410
|
} catch (error) {
|
|
301
411
|
console.error("[RedisStorageBackend] Failed to remove session:", error);
|
|
302
412
|
}
|
|
@@ -311,7 +421,7 @@ var RedisStorageBackend = class {
|
|
|
311
421
|
return null;
|
|
312
422
|
}
|
|
313
423
|
try {
|
|
314
|
-
return JSON.parse(data).sessionId;
|
|
424
|
+
return normalizeStoredSession(JSON.parse(data)).sessionId;
|
|
315
425
|
} catch (error) {
|
|
316
426
|
console.error("[RedisStorageBackend] Failed to parse session while listing all session IDs:", error);
|
|
317
427
|
return null;
|
|
@@ -327,8 +437,9 @@ var RedisStorageBackend = class {
|
|
|
327
437
|
async clearAll() {
|
|
328
438
|
try {
|
|
329
439
|
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
440
|
+
const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
|
|
330
441
|
const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
|
|
331
|
-
const allKeys = [...keys, ...userIdKeys];
|
|
442
|
+
const allKeys = [...keys, ...credentialKeys, ...userIdKeys];
|
|
332
443
|
if (allKeys.length > 0) {
|
|
333
444
|
await this.redis.del(...allKeys);
|
|
334
445
|
}
|
|
@@ -352,6 +463,7 @@ var RedisStorageBackend = class {
|
|
|
352
463
|
const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
|
|
353
464
|
if (staleSessionIds.length > 0) {
|
|
354
465
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
466
|
+
await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
|
|
355
467
|
}
|
|
356
468
|
const remainingCount = await this.redis.scard(userIdKey);
|
|
357
469
|
if (remainingCount === 0) {
|
|
@@ -376,6 +488,7 @@ var MemoryStorageBackend = class {
|
|
|
376
488
|
constructor() {
|
|
377
489
|
// Map<userId:sessionId, Session>
|
|
378
490
|
__publicField(this, "sessions", /* @__PURE__ */ new Map());
|
|
491
|
+
__publicField(this, "credentials", /* @__PURE__ */ new Map());
|
|
379
492
|
// Map<userId, Set<sessionId>>
|
|
380
493
|
__publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
|
|
381
494
|
}
|
|
@@ -388,36 +501,55 @@ var MemoryStorageBackend = class {
|
|
|
388
501
|
generateSessionId() {
|
|
389
502
|
return generateSessionId();
|
|
390
503
|
}
|
|
391
|
-
async create(session
|
|
504
|
+
async create(session) {
|
|
392
505
|
const { sessionId, userId } = session;
|
|
393
506
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
394
507
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
395
508
|
if (this.sessions.has(sessionKey)) {
|
|
396
509
|
throw new Error(`Session ${sessionId} already exists`);
|
|
397
510
|
}
|
|
398
|
-
this.sessions.set(sessionKey, session);
|
|
511
|
+
this.sessions.set(sessionKey, normalizeNewSession(session));
|
|
399
512
|
if (!this.userIdSessions.has(userId)) {
|
|
400
513
|
this.userIdSessions.set(userId, /* @__PURE__ */ new Set());
|
|
401
514
|
}
|
|
402
515
|
this.userIdSessions.get(userId).add(sessionId);
|
|
403
516
|
}
|
|
404
|
-
async update(userId, sessionId, data
|
|
517
|
+
async update(userId, sessionId, data) {
|
|
405
518
|
if (!userId || !sessionId) throw new Error("userId and sessionId required");
|
|
406
519
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
407
520
|
const current = this.sessions.get(sessionKey);
|
|
408
521
|
if (!current) {
|
|
409
522
|
throw new Error(`Session ${sessionId} not found`);
|
|
410
523
|
}
|
|
411
|
-
const updated =
|
|
412
|
-
...current,
|
|
413
|
-
...data
|
|
414
|
-
};
|
|
524
|
+
const updated = mergeSessionUpdate(current, data);
|
|
415
525
|
this.sessions.set(sessionKey, updated);
|
|
416
526
|
}
|
|
527
|
+
async patchCredentials(userId, sessionId, data) {
|
|
528
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
529
|
+
if (!this.sessions.has(sessionKey)) {
|
|
530
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
531
|
+
}
|
|
532
|
+
const current = this.credentials.get(sessionKey) ?? { sessionId, userId };
|
|
533
|
+
this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
534
|
+
}
|
|
417
535
|
async get(userId, sessionId) {
|
|
418
536
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
419
537
|
return this.sessions.get(sessionKey) || null;
|
|
420
538
|
}
|
|
539
|
+
async getCredentials(userId, sessionId) {
|
|
540
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
541
|
+
if (!this.sessions.has(sessionKey)) return null;
|
|
542
|
+
return this.credentials.get(sessionKey) ?? { sessionId, userId };
|
|
543
|
+
}
|
|
544
|
+
async clearCredentials(userId, sessionId) {
|
|
545
|
+
await this.patchCredentials(userId, sessionId, {
|
|
546
|
+
clientInformation: null,
|
|
547
|
+
tokens: null,
|
|
548
|
+
codeVerifier: null,
|
|
549
|
+
clientId: null,
|
|
550
|
+
oauthState: null
|
|
551
|
+
});
|
|
552
|
+
}
|
|
421
553
|
async listIds(userId) {
|
|
422
554
|
const set = this.userIdSessions.get(userId);
|
|
423
555
|
return set ? Array.from(set) : [];
|
|
@@ -437,6 +569,7 @@ var MemoryStorageBackend = class {
|
|
|
437
569
|
async delete(userId, sessionId) {
|
|
438
570
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
439
571
|
this.sessions.delete(sessionKey);
|
|
572
|
+
this.credentials.delete(sessionKey);
|
|
440
573
|
const set = this.userIdSessions.get(userId);
|
|
441
574
|
if (set) {
|
|
442
575
|
set.delete(sessionId);
|
|
@@ -450,9 +583,22 @@ var MemoryStorageBackend = class {
|
|
|
450
583
|
}
|
|
451
584
|
async clearAll() {
|
|
452
585
|
this.sessions.clear();
|
|
586
|
+
this.credentials.clear();
|
|
453
587
|
this.userIdSessions.clear();
|
|
454
588
|
}
|
|
455
589
|
async cleanupExpired() {
|
|
590
|
+
for (const [key, session] of this.sessions.entries()) {
|
|
591
|
+
if (!isSessionExpired(session)) continue;
|
|
592
|
+
this.sessions.delete(key);
|
|
593
|
+
this.credentials.delete(key);
|
|
594
|
+
const set = this.userIdSessions.get(session.userId);
|
|
595
|
+
if (set) {
|
|
596
|
+
set.delete(session.sessionId);
|
|
597
|
+
if (set.size === 0) {
|
|
598
|
+
this.userIdSessions.delete(session.userId);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
456
602
|
}
|
|
457
603
|
async disconnect() {
|
|
458
604
|
}
|
|
@@ -464,6 +610,7 @@ var FileStorageBackend = class {
|
|
|
464
610
|
constructor(options = {}) {
|
|
465
611
|
__publicField(this, "filePath");
|
|
466
612
|
__publicField(this, "memoryCache", null);
|
|
613
|
+
__publicField(this, "credentialsCache", null);
|
|
467
614
|
__publicField(this, "initialized", false);
|
|
468
615
|
this.filePath = options.path || "./sessions.json";
|
|
469
616
|
}
|
|
@@ -478,14 +625,22 @@ var FileStorageBackend = class {
|
|
|
478
625
|
const data = await promises.readFile(this.filePath, "utf-8");
|
|
479
626
|
const json = JSON.parse(data);
|
|
480
627
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
628
|
+
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
629
|
+
if (Array.isArray(json.sessions)) {
|
|
630
|
+
json.sessions.forEach((s) => {
|
|
631
|
+
const session = normalizeStoredSession(s);
|
|
632
|
+
this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (Array.isArray(json.credentials)) {
|
|
636
|
+
json.credentials.forEach((c) => {
|
|
637
|
+
this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
|
|
484
638
|
});
|
|
485
639
|
}
|
|
486
640
|
} catch (error) {
|
|
487
641
|
if (error.code === "ENOENT") {
|
|
488
642
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
643
|
+
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
489
644
|
await this.flush();
|
|
490
645
|
} else {
|
|
491
646
|
console.error("[FileStorage] Failed to load sessions:", error);
|
|
@@ -499,9 +654,11 @@ var FileStorageBackend = class {
|
|
|
499
654
|
if (!this.initialized) await this.init();
|
|
500
655
|
}
|
|
501
656
|
async flush() {
|
|
502
|
-
if (!this.memoryCache) return;
|
|
503
|
-
|
|
504
|
-
|
|
657
|
+
if (!this.memoryCache || !this.credentialsCache) return;
|
|
658
|
+
await promises.writeFile(this.filePath, JSON.stringify({
|
|
659
|
+
sessions: Array.from(this.memoryCache.values()),
|
|
660
|
+
credentials: Array.from(this.credentialsCache.values())
|
|
661
|
+
}, null, 2), "utf-8");
|
|
505
662
|
}
|
|
506
663
|
getSessionKey(userId, sessionId) {
|
|
507
664
|
return `${userId}:${sessionId}`;
|
|
@@ -509,7 +666,7 @@ var FileStorageBackend = class {
|
|
|
509
666
|
generateSessionId() {
|
|
510
667
|
return generateSessionId();
|
|
511
668
|
}
|
|
512
|
-
async create(session
|
|
669
|
+
async create(session) {
|
|
513
670
|
await this.ensureInitialized();
|
|
514
671
|
const { sessionId, userId } = session;
|
|
515
672
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
@@ -517,10 +674,10 @@ var FileStorageBackend = class {
|
|
|
517
674
|
if (this.memoryCache.has(sessionKey)) {
|
|
518
675
|
throw new Error(`Session ${sessionId} already exists`);
|
|
519
676
|
}
|
|
520
|
-
this.memoryCache.set(sessionKey, session);
|
|
677
|
+
this.memoryCache.set(sessionKey, normalizeNewSession(session));
|
|
521
678
|
await this.flush();
|
|
522
679
|
}
|
|
523
|
-
async update(userId, sessionId, data
|
|
680
|
+
async update(userId, sessionId, data) {
|
|
524
681
|
await this.ensureInitialized();
|
|
525
682
|
if (!userId || !sessionId) throw new Error("userId and sessionId required");
|
|
526
683
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
@@ -528,18 +685,40 @@ var FileStorageBackend = class {
|
|
|
528
685
|
if (!current) {
|
|
529
686
|
throw new Error(`Session ${sessionId} not found`);
|
|
530
687
|
}
|
|
531
|
-
const updated =
|
|
532
|
-
...current,
|
|
533
|
-
...data
|
|
534
|
-
};
|
|
688
|
+
const updated = mergeSessionUpdate(current, data);
|
|
535
689
|
this.memoryCache.set(sessionKey, updated);
|
|
536
690
|
await this.flush();
|
|
537
691
|
}
|
|
692
|
+
async patchCredentials(userId, sessionId, data) {
|
|
693
|
+
await this.ensureInitialized();
|
|
694
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
695
|
+
if (!this.memoryCache.has(sessionKey)) {
|
|
696
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
697
|
+
}
|
|
698
|
+
const current = this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
|
|
699
|
+
this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
700
|
+
await this.flush();
|
|
701
|
+
}
|
|
538
702
|
async get(userId, sessionId) {
|
|
539
703
|
await this.ensureInitialized();
|
|
540
704
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
541
705
|
return this.memoryCache.get(sessionKey) || null;
|
|
542
706
|
}
|
|
707
|
+
async getCredentials(userId, sessionId) {
|
|
708
|
+
await this.ensureInitialized();
|
|
709
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
710
|
+
if (!this.memoryCache.has(sessionKey)) return null;
|
|
711
|
+
return this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
|
|
712
|
+
}
|
|
713
|
+
async clearCredentials(userId, sessionId) {
|
|
714
|
+
await this.patchCredentials(userId, sessionId, {
|
|
715
|
+
clientInformation: null,
|
|
716
|
+
tokens: null,
|
|
717
|
+
codeVerifier: null,
|
|
718
|
+
clientId: null,
|
|
719
|
+
oauthState: null
|
|
720
|
+
});
|
|
721
|
+
}
|
|
543
722
|
async list(userId) {
|
|
544
723
|
await this.ensureInitialized();
|
|
545
724
|
return Array.from(this.memoryCache.values()).filter((s) => s.userId === userId);
|
|
@@ -551,7 +730,9 @@ var FileStorageBackend = class {
|
|
|
551
730
|
async delete(userId, sessionId) {
|
|
552
731
|
await this.ensureInitialized();
|
|
553
732
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
554
|
-
|
|
733
|
+
const deleted = this.memoryCache.delete(sessionKey);
|
|
734
|
+
this.credentialsCache.delete(sessionKey);
|
|
735
|
+
if (deleted) {
|
|
555
736
|
await this.flush();
|
|
556
737
|
}
|
|
557
738
|
}
|
|
@@ -562,10 +743,21 @@ var FileStorageBackend = class {
|
|
|
562
743
|
async clearAll() {
|
|
563
744
|
await this.ensureInitialized();
|
|
564
745
|
this.memoryCache.clear();
|
|
746
|
+
this.credentialsCache.clear();
|
|
565
747
|
await this.flush();
|
|
566
748
|
}
|
|
567
749
|
async cleanupExpired() {
|
|
568
750
|
await this.ensureInitialized();
|
|
751
|
+
let changed = false;
|
|
752
|
+
for (const [key, session] of this.memoryCache.entries()) {
|
|
753
|
+
if (!isSessionExpired(session)) continue;
|
|
754
|
+
this.memoryCache.delete(key);
|
|
755
|
+
this.credentialsCache.delete(key);
|
|
756
|
+
changed = true;
|
|
757
|
+
}
|
|
758
|
+
if (changed) {
|
|
759
|
+
await this.flush();
|
|
760
|
+
}
|
|
569
761
|
}
|
|
570
762
|
async disconnect() {
|
|
571
763
|
}
|
|
@@ -574,10 +766,12 @@ var SqliteStorage = class {
|
|
|
574
766
|
constructor(options = {}) {
|
|
575
767
|
__publicField(this, "db", null);
|
|
576
768
|
__publicField(this, "table");
|
|
769
|
+
__publicField(this, "credentialsTable");
|
|
577
770
|
__publicField(this, "initialized", false);
|
|
578
771
|
__publicField(this, "dbPath");
|
|
579
772
|
this.dbPath = options.path || "./sessions.db";
|
|
580
773
|
this.table = options.table || "mcp_sessions";
|
|
774
|
+
this.credentialsTable = `${this.table}_credentials`;
|
|
581
775
|
}
|
|
582
776
|
async init() {
|
|
583
777
|
if (this.initialized) return;
|
|
@@ -588,6 +782,7 @@ var SqliteStorage = class {
|
|
|
588
782
|
fs2.mkdirSync(dir, { recursive: true });
|
|
589
783
|
}
|
|
590
784
|
this.db = new DatabaseConstructor(this.dbPath);
|
|
785
|
+
this.db.pragma("foreign_keys = ON");
|
|
591
786
|
this.db.exec(`
|
|
592
787
|
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
593
788
|
sessionId TEXT PRIMARY KEY,
|
|
@@ -596,6 +791,13 @@ var SqliteStorage = class {
|
|
|
596
791
|
expiresAt INTEGER
|
|
597
792
|
);
|
|
598
793
|
CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
|
|
794
|
+
CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
|
|
795
|
+
sessionId TEXT NOT NULL,
|
|
796
|
+
userId TEXT NOT NULL,
|
|
797
|
+
data TEXT NOT NULL,
|
|
798
|
+
PRIMARY KEY (userId, sessionId),
|
|
799
|
+
FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
|
|
800
|
+
);
|
|
599
801
|
`);
|
|
600
802
|
this.initialized = true;
|
|
601
803
|
console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
|
|
@@ -616,18 +818,18 @@ var SqliteStorage = class {
|
|
|
616
818
|
generateSessionId() {
|
|
617
819
|
return generateSessionId();
|
|
618
820
|
}
|
|
619
|
-
async create(session
|
|
821
|
+
async create(session) {
|
|
620
822
|
this.ensureInitialized();
|
|
621
823
|
const { sessionId, userId } = session;
|
|
622
824
|
if (!sessionId || !userId) {
|
|
623
825
|
throw new Error("userId and sessionId required");
|
|
624
826
|
}
|
|
625
|
-
const
|
|
827
|
+
const sessionWithLifecycle = normalizeNewSession(session);
|
|
626
828
|
try {
|
|
627
829
|
const stmt = this.db.prepare(
|
|
628
830
|
`INSERT INTO ${this.table} (sessionId, userId, data, expiresAt) VALUES (?, ?, ?, ?)`
|
|
629
831
|
);
|
|
630
|
-
stmt.run(sessionId, userId, JSON.stringify(
|
|
832
|
+
stmt.run(sessionId, userId, JSON.stringify(sessionWithLifecycle), sessionWithLifecycle.expiresAt ?? null);
|
|
631
833
|
} catch (error) {
|
|
632
834
|
if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
633
835
|
throw new Error(`Session ${sessionId} already exists`);
|
|
@@ -635,7 +837,7 @@ var SqliteStorage = class {
|
|
|
635
837
|
throw error;
|
|
636
838
|
}
|
|
637
839
|
}
|
|
638
|
-
async update(userId, sessionId, data
|
|
840
|
+
async update(userId, sessionId, data) {
|
|
639
841
|
this.ensureInitialized();
|
|
640
842
|
if (!sessionId || !userId) {
|
|
641
843
|
throw new Error("userId and sessionId required");
|
|
@@ -644,12 +846,25 @@ var SqliteStorage = class {
|
|
|
644
846
|
if (!currentSession) {
|
|
645
847
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
646
848
|
}
|
|
647
|
-
const updatedSession =
|
|
648
|
-
const expiresAt = ttl ? Date.now() + ttl * 1e3 : null;
|
|
849
|
+
const updatedSession = mergeSessionUpdate(currentSession, data);
|
|
649
850
|
const stmt = this.db.prepare(
|
|
650
851
|
`UPDATE ${this.table} SET data = ?, expiresAt = ? WHERE sessionId = ? AND userId = ?`
|
|
651
852
|
);
|
|
652
|
-
stmt.run(JSON.stringify(updatedSession), expiresAt, sessionId, userId);
|
|
853
|
+
stmt.run(JSON.stringify(updatedSession), updatedSession.expiresAt ?? null, sessionId, userId);
|
|
854
|
+
}
|
|
855
|
+
async patchCredentials(userId, sessionId, data) {
|
|
856
|
+
this.ensureInitialized();
|
|
857
|
+
if (!await this.get(userId, sessionId)) {
|
|
858
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
859
|
+
}
|
|
860
|
+
const current = await this.getCredentials(userId, sessionId) ?? { sessionId, userId };
|
|
861
|
+
const credentials = { ...current, ...data, sessionId, userId };
|
|
862
|
+
const stmt = this.db.prepare(
|
|
863
|
+
`INSERT INTO ${this.credentialsTable} (sessionId, userId, data)
|
|
864
|
+
VALUES (?, ?, ?)
|
|
865
|
+
ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
|
|
866
|
+
);
|
|
867
|
+
stmt.run(sessionId, userId, JSON.stringify(credentials));
|
|
653
868
|
}
|
|
654
869
|
async get(userId, sessionId) {
|
|
655
870
|
this.ensureInitialized();
|
|
@@ -658,7 +873,25 @@ var SqliteStorage = class {
|
|
|
658
873
|
);
|
|
659
874
|
const row = stmt.get(sessionId, userId);
|
|
660
875
|
if (!row) return null;
|
|
661
|
-
return JSON.parse(row.data);
|
|
876
|
+
return normalizeStoredSession(JSON.parse(row.data));
|
|
877
|
+
}
|
|
878
|
+
async getCredentials(userId, sessionId) {
|
|
879
|
+
this.ensureInitialized();
|
|
880
|
+
if (!await this.get(userId, sessionId)) return null;
|
|
881
|
+
const stmt = this.db.prepare(
|
|
882
|
+
`SELECT data FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
883
|
+
);
|
|
884
|
+
const row = stmt.get(sessionId, userId);
|
|
885
|
+
return row ? JSON.parse(row.data) : { sessionId, userId };
|
|
886
|
+
}
|
|
887
|
+
async clearCredentials(userId, sessionId) {
|
|
888
|
+
await this.patchCredentials(userId, sessionId, {
|
|
889
|
+
clientInformation: null,
|
|
890
|
+
tokens: null,
|
|
891
|
+
codeVerifier: null,
|
|
892
|
+
clientId: null,
|
|
893
|
+
oauthState: null
|
|
894
|
+
});
|
|
662
895
|
}
|
|
663
896
|
async list(userId) {
|
|
664
897
|
this.ensureInitialized();
|
|
@@ -666,7 +899,7 @@ var SqliteStorage = class {
|
|
|
666
899
|
`SELECT data FROM ${this.table} WHERE userId = ?`
|
|
667
900
|
);
|
|
668
901
|
const rows = stmt.all(userId);
|
|
669
|
-
return rows.map((row) => JSON.parse(row.data));
|
|
902
|
+
return rows.map((row) => normalizeStoredSession(JSON.parse(row.data)));
|
|
670
903
|
}
|
|
671
904
|
async listIds(userId) {
|
|
672
905
|
this.ensureInitialized();
|
|
@@ -681,6 +914,9 @@ var SqliteStorage = class {
|
|
|
681
914
|
const stmt = this.db.prepare(
|
|
682
915
|
`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
683
916
|
);
|
|
917
|
+
this.db.prepare(
|
|
918
|
+
`DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
919
|
+
).run(sessionId, userId);
|
|
684
920
|
stmt.run(sessionId, userId);
|
|
685
921
|
}
|
|
686
922
|
async listAllIds() {
|
|
@@ -691,16 +927,28 @@ var SqliteStorage = class {
|
|
|
691
927
|
}
|
|
692
928
|
async clearAll() {
|
|
693
929
|
this.ensureInitialized();
|
|
930
|
+
this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
|
|
694
931
|
const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
|
|
695
932
|
stmt.run();
|
|
696
933
|
}
|
|
697
934
|
async cleanupExpired() {
|
|
698
935
|
this.ensureInitialized();
|
|
699
|
-
const
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
936
|
+
const rows = this.db.prepare(`SELECT sessionId, userId, data FROM ${this.table}`).all();
|
|
937
|
+
const deleteStmt = this.db.prepare(`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`);
|
|
938
|
+
for (const row of rows) {
|
|
939
|
+
const session = normalizeStoredSession(JSON.parse(row.data));
|
|
940
|
+
if (isSessionExpired(session)) {
|
|
941
|
+
deleteStmt.run(row.sessionId, row.userId);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
this.db.prepare(
|
|
945
|
+
`DELETE FROM ${this.credentialsTable}
|
|
946
|
+
WHERE NOT EXISTS (
|
|
947
|
+
SELECT 1 FROM ${this.table}
|
|
948
|
+
WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
|
|
949
|
+
AND ${this.table}.userId = ${this.credentialsTable}.userId
|
|
950
|
+
)`
|
|
951
|
+
).run();
|
|
704
952
|
}
|
|
705
953
|
async disconnect() {
|
|
706
954
|
if (this.db) {
|
|
@@ -779,19 +1027,21 @@ function decryptObject(data) {
|
|
|
779
1027
|
var SupabaseStorageBackend = class {
|
|
780
1028
|
constructor(supabase) {
|
|
781
1029
|
this.supabase = supabase;
|
|
782
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
783
1030
|
}
|
|
784
1031
|
async init() {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1032
|
+
await this.assertTable("mcp_sessions", "session_id");
|
|
1033
|
+
await this.assertTable("mcp_credentials", "session_id");
|
|
1034
|
+
console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
|
|
1035
|
+
}
|
|
1036
|
+
async assertTable(table, column) {
|
|
1037
|
+
const { error } = await this.supabase.from(table).select(column).limit(0);
|
|
1038
|
+
if (!error) return;
|
|
1039
|
+
if (error.code === "42P01") {
|
|
1040
|
+
throw new Error(
|
|
1041
|
+
`[SupabaseStorage] Table "${table}" not found in your database. Please run "npx mcp-ts supabase-init" to set up the required storage schema.`
|
|
1042
|
+
);
|
|
793
1043
|
}
|
|
794
|
-
|
|
1044
|
+
throw new Error(`[SupabaseStorage] Initialization check failed for "${table}": ${error.message}`);
|
|
795
1045
|
}
|
|
796
1046
|
generateSessionId() {
|
|
797
1047
|
return generateSessionId();
|
|
@@ -805,37 +1055,49 @@ var SupabaseStorageBackend = class {
|
|
|
805
1055
|
transportType: row.transport_type,
|
|
806
1056
|
callbackUrl: row.callback_url,
|
|
807
1057
|
createdAt: new Date(row.created_at).getTime(),
|
|
1058
|
+
updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
|
|
1059
|
+
expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
|
|
808
1060
|
userId: row.user_id,
|
|
809
1061
|
headers: decryptObject(row.headers),
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1062
|
+
authUrl: row.auth_url,
|
|
1063
|
+
status: row.status ?? "pending"
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
mapRowToCredentials(row, userId, sessionId) {
|
|
1067
|
+
return {
|
|
1068
|
+
sessionId,
|
|
1069
|
+
userId,
|
|
1070
|
+
clientInformation: decryptObject(row?.client_information),
|
|
1071
|
+
tokens: decryptObject(row?.tokens),
|
|
1072
|
+
codeVerifier: decryptObject(row?.code_verifier),
|
|
1073
|
+
clientId: row?.client_id,
|
|
1074
|
+
oauthState: row?.oauth_state
|
|
815
1075
|
};
|
|
816
1076
|
}
|
|
817
|
-
|
|
1077
|
+
hasCredentialData(data) {
|
|
1078
|
+
return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
|
|
1079
|
+
}
|
|
1080
|
+
async create(session) {
|
|
818
1081
|
const { sessionId, userId } = session;
|
|
819
1082
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
820
|
-
const
|
|
821
|
-
const
|
|
1083
|
+
const status = session.status ?? "pending";
|
|
1084
|
+
const createdAt = new Date(session.createdAt || Date.now()).toISOString();
|
|
1085
|
+
const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
|
|
1086
|
+
const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
|
|
822
1087
|
const { error } = await this.supabase.from("mcp_sessions").insert({
|
|
823
1088
|
session_id: sessionId,
|
|
824
1089
|
user_id: userId,
|
|
825
|
-
// Maps user_id to userId to support RLS using auth.uid()
|
|
826
1090
|
server_id: session.serverId,
|
|
827
1091
|
server_name: session.serverName,
|
|
828
1092
|
server_url: session.serverUrl,
|
|
829
1093
|
transport_type: session.transportType,
|
|
830
1094
|
callback_url: session.callbackUrl,
|
|
831
|
-
created_at:
|
|
1095
|
+
created_at: createdAt,
|
|
1096
|
+
updated_at: updatedAt,
|
|
832
1097
|
headers: encryptObject(session.headers),
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
code_verifier: session.codeVerifier,
|
|
837
|
-
client_id: session.clientId,
|
|
838
|
-
expires_at: expiresAt
|
|
1098
|
+
auth_url: session.authUrl ?? null,
|
|
1099
|
+
status,
|
|
1100
|
+
expires_at: expiresAt === null ? null : new Date(expiresAt).toISOString()
|
|
839
1101
|
});
|
|
840
1102
|
if (error) {
|
|
841
1103
|
if (error.code === "23505") {
|
|
@@ -844,11 +1106,8 @@ var SupabaseStorageBackend = class {
|
|
|
844
1106
|
throw new Error(`Failed to create session in Supabase: ${error.message}`);
|
|
845
1107
|
}
|
|
846
1108
|
}
|
|
847
|
-
async update(userId, sessionId, data
|
|
848
|
-
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
849
|
-
const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
|
|
1109
|
+
async update(userId, sessionId, data) {
|
|
850
1110
|
const updateData = {
|
|
851
|
-
expires_at: expiresAt,
|
|
852
1111
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
853
1112
|
};
|
|
854
1113
|
if ("serverId" in data) updateData.server_id = data.serverId;
|
|
@@ -856,20 +1115,50 @@ var SupabaseStorageBackend = class {
|
|
|
856
1115
|
if ("serverUrl" in data) updateData.server_url = data.serverUrl;
|
|
857
1116
|
if ("transportType" in data) updateData.transport_type = data.transportType;
|
|
858
1117
|
if ("callbackUrl" in data) updateData.callback_url = data.callbackUrl;
|
|
859
|
-
if ("
|
|
1118
|
+
if ("status" in data) {
|
|
1119
|
+
const status = data.status ?? "pending";
|
|
1120
|
+
const expiresAt = resolveSessionExpiresAt(status);
|
|
1121
|
+
updateData.status = status;
|
|
1122
|
+
updateData.expires_at = expiresAt === null ? null : new Date(expiresAt).toISOString();
|
|
1123
|
+
}
|
|
860
1124
|
if ("headers" in data) updateData.headers = encryptObject(data.headers);
|
|
861
|
-
if ("
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
if (
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1125
|
+
if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
|
|
1126
|
+
const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
|
|
1127
|
+
let updatedRows = null;
|
|
1128
|
+
if (shouldUpdateSession) {
|
|
1129
|
+
const result = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId).select("id");
|
|
1130
|
+
if (result.error) {
|
|
1131
|
+
throw new Error(`Failed to update session: ${result.error.message}`);
|
|
1132
|
+
}
|
|
1133
|
+
updatedRows = result.data;
|
|
1134
|
+
} else {
|
|
1135
|
+
const result = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
|
|
1136
|
+
if (result.error) {
|
|
1137
|
+
throw new Error(`Failed to update session: ${result.error.message}`);
|
|
1138
|
+
}
|
|
1139
|
+
updatedRows = result.data;
|
|
868
1140
|
}
|
|
869
1141
|
if (!updatedRows || updatedRows.length === 0) {
|
|
870
1142
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
871
1143
|
}
|
|
872
1144
|
}
|
|
1145
|
+
async patchCredentials(userId, sessionId, data) {
|
|
1146
|
+
if (!this.hasCredentialData(data)) return;
|
|
1147
|
+
const row = {
|
|
1148
|
+
user_id: userId,
|
|
1149
|
+
session_id: sessionId,
|
|
1150
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1151
|
+
};
|
|
1152
|
+
if ("clientInformation" in data) row.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
|
|
1153
|
+
if ("tokens" in data) row.tokens = data.tokens == null ? null : encryptObject(data.tokens);
|
|
1154
|
+
if ("codeVerifier" in data) row.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
|
|
1155
|
+
if ("clientId" in data) row.client_id = data.clientId ?? null;
|
|
1156
|
+
if ("oauthState" in data) row.oauth_state = data.oauthState ?? null;
|
|
1157
|
+
const { error } = await this.supabase.from("mcp_credentials").upsert(row, { onConflict: "user_id,session_id" });
|
|
1158
|
+
if (error) {
|
|
1159
|
+
throw new Error(`Failed to update credentials: ${error.message}`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
873
1162
|
async get(userId, sessionId) {
|
|
874
1163
|
const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
875
1164
|
if (error) {
|
|
@@ -879,6 +1168,21 @@ var SupabaseStorageBackend = class {
|
|
|
879
1168
|
if (!data) return null;
|
|
880
1169
|
return this.mapRowToSessionData(data);
|
|
881
1170
|
}
|
|
1171
|
+
async getCredentials(userId, sessionId) {
|
|
1172
|
+
const { data, error } = await this.supabase.from("mcp_credentials").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
1173
|
+
if (error) {
|
|
1174
|
+
console.error("[SupabaseStorage] Failed to get credentials:", error);
|
|
1175
|
+
return null;
|
|
1176
|
+
}
|
|
1177
|
+
if (data) {
|
|
1178
|
+
return this.mapRowToCredentials(data, userId, sessionId);
|
|
1179
|
+
}
|
|
1180
|
+
const { data: sessionRows, error: sessionError } = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
|
|
1181
|
+
if (sessionError || !sessionRows || sessionRows.length === 0) {
|
|
1182
|
+
return null;
|
|
1183
|
+
}
|
|
1184
|
+
return { sessionId, userId };
|
|
1185
|
+
}
|
|
882
1186
|
async list(userId) {
|
|
883
1187
|
const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
|
|
884
1188
|
if (error) {
|
|
@@ -887,6 +1191,12 @@ var SupabaseStorageBackend = class {
|
|
|
887
1191
|
}
|
|
888
1192
|
return data.map((row) => this.mapRowToSessionData(row));
|
|
889
1193
|
}
|
|
1194
|
+
async clearCredentials(userId, sessionId) {
|
|
1195
|
+
const { error } = await this.supabase.from("mcp_credentials").delete().eq("user_id", userId).eq("session_id", sessionId);
|
|
1196
|
+
if (error) {
|
|
1197
|
+
throw new Error(`Failed to clear credentials: ${error.message}`);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
890
1200
|
async delete(userId, sessionId) {
|
|
891
1201
|
const { error } = await this.supabase.from("mcp_sessions").delete().eq("user_id", userId).eq("session_id", sessionId);
|
|
892
1202
|
if (error) {
|
|
@@ -910,15 +1220,24 @@ var SupabaseStorageBackend = class {
|
|
|
910
1220
|
return data.map((row) => row.session_id);
|
|
911
1221
|
}
|
|
912
1222
|
async clearAll() {
|
|
1223
|
+
const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
|
|
1224
|
+
if (credentialsError) {
|
|
1225
|
+
console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
|
|
1226
|
+
}
|
|
913
1227
|
const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
|
|
914
1228
|
if (error) {
|
|
915
1229
|
console.error("[SupabaseStorage] Failed to clear sessions:", error);
|
|
916
1230
|
}
|
|
917
1231
|
}
|
|
918
1232
|
async cleanupExpired() {
|
|
919
|
-
const { error } = await this.supabase.from("mcp_sessions").delete().lt("expires_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
920
|
-
if (
|
|
921
|
-
console.error("[SupabaseStorage] Failed to cleanup expired sessions:",
|
|
1233
|
+
const { error: transientError } = await this.supabase.from("mcp_sessions").delete().not("expires_at", "is", null).neq("status", "active").lt("expires_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
1234
|
+
if (transientError) {
|
|
1235
|
+
console.error("[SupabaseStorage] Failed to cleanup expired inactive sessions:", transientError);
|
|
1236
|
+
}
|
|
1237
|
+
const dormantCutoff = new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString();
|
|
1238
|
+
const { error: dormantError } = await this.supabase.from("mcp_sessions").delete().eq("status", "active").lt("updated_at", dormantCutoff);
|
|
1239
|
+
if (dormantError) {
|
|
1240
|
+
console.error("[SupabaseStorage] Failed to cleanup dormant active sessions:", dormantError);
|
|
922
1241
|
}
|
|
923
1242
|
}
|
|
924
1243
|
async disconnect() {
|
|
@@ -929,23 +1248,29 @@ var SupabaseStorageBackend = class {
|
|
|
929
1248
|
var NeonStorageBackend = class {
|
|
930
1249
|
constructor(sql, options = {}) {
|
|
931
1250
|
this.sql = sql;
|
|
932
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
933
1251
|
__publicField(this, "tableName");
|
|
1252
|
+
__publicField(this, "credentialsTableName");
|
|
934
1253
|
const schema = options.schema || "public";
|
|
935
1254
|
const table = options.table || "mcp_sessions";
|
|
1255
|
+
const credentialsTable = options.credentialsTable || "mcp_credentials";
|
|
936
1256
|
this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
|
|
1257
|
+
this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
|
|
937
1258
|
}
|
|
938
1259
|
async init() {
|
|
1260
|
+
await this.assertTable(this.tableName, "mcp_sessions");
|
|
1261
|
+
await this.assertTable(this.credentialsTableName, "mcp_credentials");
|
|
1262
|
+
console.log("[mcp-ts][Storage] Neon: storage tables verified.");
|
|
1263
|
+
}
|
|
1264
|
+
async assertTable(qualifiedName, displayName) {
|
|
939
1265
|
const [{ exists } = { exists: null }] = await this.sql.query(
|
|
940
1266
|
"SELECT to_regclass($1) AS exists",
|
|
941
|
-
[
|
|
1267
|
+
[qualifiedName.replace(/"/g, "")]
|
|
942
1268
|
);
|
|
943
1269
|
if (!exists) {
|
|
944
1270
|
throw new Error(
|
|
945
|
-
|
|
1271
|
+
`[NeonStorage] Table "${displayName}" not found in your database. Please create it using the Neon storage guide in docs/storage-backends/neon.md.`
|
|
946
1272
|
);
|
|
947
1273
|
}
|
|
948
|
-
console.log('[mcp-ts][Storage] Neon: "mcp_sessions" table verified.');
|
|
949
1274
|
}
|
|
950
1275
|
generateSessionId() {
|
|
951
1276
|
return generateSessionId();
|
|
@@ -965,20 +1290,35 @@ var NeonStorageBackend = class {
|
|
|
965
1290
|
transportType: row.transport_type,
|
|
966
1291
|
callbackUrl: row.callback_url,
|
|
967
1292
|
createdAt: new Date(row.created_at).getTime(),
|
|
1293
|
+
updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
|
|
1294
|
+
expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
|
|
968
1295
|
userId: row.user_id,
|
|
969
1296
|
headers: decryptObject(row.headers),
|
|
970
|
-
|
|
971
|
-
|
|
1297
|
+
authUrl: row.auth_url ?? void 0,
|
|
1298
|
+
status: row.status ?? "pending"
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
mapRowToCredentials(row, userId, sessionId) {
|
|
1302
|
+
return {
|
|
1303
|
+
sessionId,
|
|
1304
|
+
userId,
|
|
1305
|
+
clientInformation: decryptObject(row.client_information),
|
|
972
1306
|
tokens: decryptObject(row.tokens),
|
|
973
|
-
codeVerifier: row.code_verifier
|
|
974
|
-
clientId: row.client_id ?? void 0
|
|
1307
|
+
codeVerifier: decryptObject(row.code_verifier),
|
|
1308
|
+
clientId: row.client_id ?? void 0,
|
|
1309
|
+
oauthState: row.oauth_state
|
|
975
1310
|
};
|
|
976
1311
|
}
|
|
977
|
-
|
|
1312
|
+
hasCredentialData(data) {
|
|
1313
|
+
return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
|
|
1314
|
+
}
|
|
1315
|
+
async create(session) {
|
|
978
1316
|
const { sessionId, userId } = session;
|
|
979
1317
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
980
|
-
const
|
|
981
|
-
const
|
|
1318
|
+
const status = session.status ?? "pending";
|
|
1319
|
+
const createdAt = new Date(session.createdAt || Date.now()).toISOString();
|
|
1320
|
+
const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
|
|
1321
|
+
const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
|
|
982
1322
|
try {
|
|
983
1323
|
await this.sql.query(
|
|
984
1324
|
`INSERT INTO ${this.tableName} (
|
|
@@ -990,16 +1330,14 @@ var NeonStorageBackend = class {
|
|
|
990
1330
|
transport_type,
|
|
991
1331
|
callback_url,
|
|
992
1332
|
created_at,
|
|
1333
|
+
updated_at,
|
|
993
1334
|
headers,
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
tokens,
|
|
997
|
-
code_verifier,
|
|
998
|
-
client_id,
|
|
1335
|
+
auth_url,
|
|
1336
|
+
status,
|
|
999
1337
|
expires_at
|
|
1000
1338
|
) VALUES (
|
|
1001
1339
|
$1, $2, $3, $4, $5, $6, $7, $8,
|
|
1002
|
-
$9, $10, $11, $12, $13
|
|
1340
|
+
$9, $10, $11, $12, $13
|
|
1003
1341
|
)`,
|
|
1004
1342
|
[
|
|
1005
1343
|
sessionId,
|
|
@@ -1009,14 +1347,12 @@ var NeonStorageBackend = class {
|
|
|
1009
1347
|
session.serverUrl,
|
|
1010
1348
|
session.transportType,
|
|
1011
1349
|
session.callbackUrl,
|
|
1012
|
-
|
|
1350
|
+
createdAt,
|
|
1351
|
+
updatedAt,
|
|
1013
1352
|
encryptObject(session.headers),
|
|
1014
|
-
session.
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
session.codeVerifier,
|
|
1018
|
-
session.clientId,
|
|
1019
|
-
expiresAt
|
|
1353
|
+
session.authUrl ?? null,
|
|
1354
|
+
status,
|
|
1355
|
+
expiresAt === null ? null : new Date(expiresAt).toISOString()
|
|
1020
1356
|
]
|
|
1021
1357
|
);
|
|
1022
1358
|
} catch (error) {
|
|
@@ -1026,52 +1362,86 @@ var NeonStorageBackend = class {
|
|
|
1026
1362
|
throw new Error(`Failed to create session in Neon: ${error.message}`);
|
|
1027
1363
|
}
|
|
1028
1364
|
}
|
|
1029
|
-
async update(userId, sessionId, data
|
|
1365
|
+
async update(userId, sessionId, data) {
|
|
1030
1366
|
const currentSession = await this.get(userId, sessionId);
|
|
1031
1367
|
if (!currentSession) {
|
|
1032
1368
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1033
1369
|
}
|
|
1034
1370
|
const updatedSession = { ...currentSession, ...data };
|
|
1035
|
-
const
|
|
1036
|
-
const expiresAt =
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1371
|
+
const status = updatedSession.status ?? "pending";
|
|
1372
|
+
const expiresAt = resolveSessionExpiresAt(status);
|
|
1373
|
+
const shouldUpdateSession = "serverId" in data || "serverName" in data || "serverUrl" in data || "transportType" in data || "callbackUrl" in data || "status" in data || "headers" in data || "authUrl" in data;
|
|
1374
|
+
if (shouldUpdateSession) {
|
|
1375
|
+
const updatedRows = await this.sql.query(
|
|
1376
|
+
`UPDATE ${this.tableName}
|
|
1377
|
+
SET
|
|
1378
|
+
server_id = $1,
|
|
1379
|
+
server_name = $2,
|
|
1380
|
+
server_url = $3,
|
|
1381
|
+
transport_type = $4,
|
|
1382
|
+
callback_url = $5,
|
|
1383
|
+
status = $6,
|
|
1384
|
+
headers = $7,
|
|
1385
|
+
auth_url = $8,
|
|
1386
|
+
expires_at = $9,
|
|
1387
|
+
updated_at = now()
|
|
1388
|
+
WHERE user_id = $10 AND session_id = $11
|
|
1389
|
+
RETURNING id`,
|
|
1390
|
+
[
|
|
1391
|
+
updatedSession.serverId,
|
|
1392
|
+
updatedSession.serverName,
|
|
1393
|
+
updatedSession.serverUrl,
|
|
1394
|
+
updatedSession.transportType,
|
|
1395
|
+
updatedSession.callbackUrl,
|
|
1396
|
+
status,
|
|
1397
|
+
encryptObject(updatedSession.headers),
|
|
1398
|
+
updatedSession.authUrl ?? null,
|
|
1399
|
+
expiresAt === null ? null : new Date(expiresAt).toISOString(),
|
|
1400
|
+
userId,
|
|
1401
|
+
sessionId
|
|
1402
|
+
]
|
|
1403
|
+
);
|
|
1404
|
+
if (updatedRows.length === 0) {
|
|
1405
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
async patchCredentials(userId, sessionId, data) {
|
|
1410
|
+
if (!this.hasCredentialData(data)) return;
|
|
1411
|
+
await this.sql.query(
|
|
1412
|
+
`INSERT INTO ${this.credentialsTableName} (
|
|
1413
|
+
user_id,
|
|
1414
|
+
session_id,
|
|
1415
|
+
client_information,
|
|
1416
|
+
tokens,
|
|
1417
|
+
code_verifier,
|
|
1418
|
+
client_id,
|
|
1419
|
+
oauth_state,
|
|
1420
|
+
updated_at
|
|
1421
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, now())
|
|
1422
|
+
ON CONFLICT (user_id, session_id)
|
|
1423
|
+
DO UPDATE SET
|
|
1424
|
+
client_information = CASE WHEN $8 THEN EXCLUDED.client_information ELSE ${this.credentialsTableName}.client_information END,
|
|
1425
|
+
tokens = CASE WHEN $9 THEN EXCLUDED.tokens ELSE ${this.credentialsTableName}.tokens END,
|
|
1426
|
+
code_verifier = CASE WHEN $10 THEN EXCLUDED.code_verifier ELSE ${this.credentialsTableName}.code_verifier END,
|
|
1427
|
+
client_id = CASE WHEN $11 THEN EXCLUDED.client_id ELSE ${this.credentialsTableName}.client_id END,
|
|
1428
|
+
oauth_state = CASE WHEN $12 THEN EXCLUDED.oauth_state ELSE ${this.credentialsTableName}.oauth_state END,
|
|
1429
|
+
updated_at = now()`,
|
|
1055
1430
|
[
|
|
1056
|
-
updatedSession.serverId,
|
|
1057
|
-
updatedSession.serverName,
|
|
1058
|
-
updatedSession.serverUrl,
|
|
1059
|
-
updatedSession.transportType,
|
|
1060
|
-
updatedSession.callbackUrl,
|
|
1061
|
-
updatedSession.active ?? false,
|
|
1062
|
-
encryptObject(updatedSession.headers),
|
|
1063
|
-
updatedSession.clientInformation,
|
|
1064
|
-
updatedSession.tokens === void 0 ? null : encryptObject(updatedSession.tokens),
|
|
1065
|
-
updatedSession.codeVerifier,
|
|
1066
|
-
updatedSession.clientId,
|
|
1067
|
-
expiresAt,
|
|
1068
1431
|
userId,
|
|
1069
|
-
sessionId
|
|
1432
|
+
sessionId,
|
|
1433
|
+
"clientInformation" in data ? data.clientInformation == null ? null : encryptObject(data.clientInformation) : null,
|
|
1434
|
+
"tokens" in data ? data.tokens == null ? null : encryptObject(data.tokens) : null,
|
|
1435
|
+
"codeVerifier" in data ? data.codeVerifier == null ? null : encryptObject(data.codeVerifier) : null,
|
|
1436
|
+
"clientId" in data ? data.clientId ?? null : null,
|
|
1437
|
+
"oauthState" in data ? data.oauthState ?? null : null,
|
|
1438
|
+
"clientInformation" in data,
|
|
1439
|
+
"tokens" in data,
|
|
1440
|
+
"codeVerifier" in data,
|
|
1441
|
+
"clientId" in data,
|
|
1442
|
+
"oauthState" in data
|
|
1070
1443
|
]
|
|
1071
1444
|
);
|
|
1072
|
-
if (updatedRows.length === 0) {
|
|
1073
|
-
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1074
|
-
}
|
|
1075
1445
|
}
|
|
1076
1446
|
async get(userId, sessionId) {
|
|
1077
1447
|
try {
|
|
@@ -1079,12 +1449,32 @@ var NeonStorageBackend = class {
|
|
|
1079
1449
|
`SELECT * FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1080
1450
|
[userId, sessionId]
|
|
1081
1451
|
);
|
|
1082
|
-
|
|
1452
|
+
if (!rows[0]) return null;
|
|
1453
|
+
return this.mapRowToSessionData(rows[0]);
|
|
1083
1454
|
} catch (error) {
|
|
1084
1455
|
console.error("[NeonStorage] Failed to get session:", error);
|
|
1085
1456
|
return null;
|
|
1086
1457
|
}
|
|
1087
1458
|
}
|
|
1459
|
+
async getCredentials(userId, sessionId) {
|
|
1460
|
+
try {
|
|
1461
|
+
const credentialRows = await this.sql.query(
|
|
1462
|
+
`SELECT * FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1463
|
+
[userId, sessionId]
|
|
1464
|
+
);
|
|
1465
|
+
if (credentialRows[0]) {
|
|
1466
|
+
return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
|
|
1467
|
+
}
|
|
1468
|
+
const sessionRows = await this.sql.query(
|
|
1469
|
+
`SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1470
|
+
[userId, sessionId]
|
|
1471
|
+
);
|
|
1472
|
+
return sessionRows[0] ? { sessionId, userId } : null;
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
console.error("[NeonStorage] Failed to get credentials:", error);
|
|
1475
|
+
return null;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1088
1478
|
async list(userId) {
|
|
1089
1479
|
try {
|
|
1090
1480
|
const rows = await this.sql.query(
|
|
@@ -1097,6 +1487,16 @@ var NeonStorageBackend = class {
|
|
|
1097
1487
|
return [];
|
|
1098
1488
|
}
|
|
1099
1489
|
}
|
|
1490
|
+
async clearCredentials(userId, sessionId) {
|
|
1491
|
+
try {
|
|
1492
|
+
await this.sql.query(
|
|
1493
|
+
`DELETE FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1494
|
+
[userId, sessionId]
|
|
1495
|
+
);
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
console.error("[NeonStorage] Failed to clear credentials:", error);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1100
1500
|
async delete(userId, sessionId) {
|
|
1101
1501
|
try {
|
|
1102
1502
|
await this.sql.query(
|
|
@@ -1132,6 +1532,7 @@ var NeonStorageBackend = class {
|
|
|
1132
1532
|
}
|
|
1133
1533
|
async clearAll() {
|
|
1134
1534
|
try {
|
|
1535
|
+
await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
|
|
1135
1536
|
await this.sql.query(`DELETE FROM ${this.tableName}`);
|
|
1136
1537
|
} catch (error) {
|
|
1137
1538
|
console.error("[NeonStorage] Failed to clear sessions:", error);
|
|
@@ -1140,9 +1541,17 @@ var NeonStorageBackend = class {
|
|
|
1140
1541
|
async cleanupExpired() {
|
|
1141
1542
|
try {
|
|
1142
1543
|
await this.sql.query(
|
|
1143
|
-
`DELETE FROM ${this.tableName}
|
|
1544
|
+
`DELETE FROM ${this.tableName}
|
|
1545
|
+
WHERE expires_at IS NOT NULL
|
|
1546
|
+
AND expires_at < $1
|
|
1547
|
+
AND status <> 'active'`,
|
|
1144
1548
|
[(/* @__PURE__ */ new Date()).toISOString()]
|
|
1145
1549
|
);
|
|
1550
|
+
await this.sql.query(
|
|
1551
|
+
`DELETE FROM ${this.tableName}
|
|
1552
|
+
WHERE status = 'active' AND updated_at < $1`,
|
|
1553
|
+
[new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString()]
|
|
1554
|
+
);
|
|
1146
1555
|
} catch (error) {
|
|
1147
1556
|
console.error("[NeonStorage] Failed to cleanup expired sessions:", error);
|
|
1148
1557
|
}
|
|
@@ -1189,26 +1598,24 @@ function emitSessionMutation(event) {
|
|
|
1189
1598
|
function createSessionMutationEvent(prop, args) {
|
|
1190
1599
|
const timestamp = Date.now();
|
|
1191
1600
|
if (prop === "create") {
|
|
1192
|
-
const [session
|
|
1601
|
+
const [session] = args;
|
|
1193
1602
|
if (!session?.userId || !session?.sessionId) return null;
|
|
1194
1603
|
return {
|
|
1195
1604
|
type: "create",
|
|
1196
1605
|
userId: session.userId,
|
|
1197
1606
|
sessionId: session.sessionId,
|
|
1198
1607
|
session,
|
|
1199
|
-
ttl,
|
|
1200
1608
|
timestamp
|
|
1201
1609
|
};
|
|
1202
1610
|
}
|
|
1203
1611
|
if (prop === "update") {
|
|
1204
|
-
const [userId, sessionId, patch
|
|
1612
|
+
const [userId, sessionId, patch] = args;
|
|
1205
1613
|
if (!userId || !sessionId) return null;
|
|
1206
1614
|
return {
|
|
1207
1615
|
type: "update",
|
|
1208
1616
|
userId,
|
|
1209
1617
|
sessionId,
|
|
1210
1618
|
patch,
|
|
1211
|
-
ttl,
|
|
1212
1619
|
timestamp
|
|
1213
1620
|
};
|
|
1214
1621
|
}
|
|
@@ -1404,7 +1811,6 @@ var StorageOAuthClientProvider = class {
|
|
|
1404
1811
|
__publicField(this, "_authUrl");
|
|
1405
1812
|
__publicField(this, "_clientId");
|
|
1406
1813
|
__publicField(this, "onRedirectCallback");
|
|
1407
|
-
__publicField(this, "tokenExpiresAt");
|
|
1408
1814
|
this.userId = options.userId;
|
|
1409
1815
|
this.serverId = options.serverId;
|
|
1410
1816
|
this.sessionId = options.sessionId;
|
|
@@ -1438,30 +1844,30 @@ var StorageOAuthClientProvider = class {
|
|
|
1438
1844
|
this._clientId = clientId_;
|
|
1439
1845
|
}
|
|
1440
1846
|
/**
|
|
1441
|
-
* Loads OAuth
|
|
1847
|
+
* Loads OAuth credentials from the session store
|
|
1442
1848
|
* @private
|
|
1443
1849
|
*/
|
|
1444
|
-
async
|
|
1445
|
-
const data = await sessions.
|
|
1850
|
+
async getCredentials() {
|
|
1851
|
+
const data = await sessions.getCredentials(this.userId, this.sessionId);
|
|
1446
1852
|
if (!data) {
|
|
1447
|
-
return {};
|
|
1853
|
+
return { userId: this.userId, sessionId: this.sessionId };
|
|
1448
1854
|
}
|
|
1449
1855
|
return data;
|
|
1450
1856
|
}
|
|
1451
1857
|
/**
|
|
1452
|
-
* Saves OAuth
|
|
1453
|
-
* @param data - Partial OAuth
|
|
1858
|
+
* Saves OAuth credentials to the session store
|
|
1859
|
+
* @param data - Partial OAuth credentials to save
|
|
1454
1860
|
* @private
|
|
1455
1861
|
* @throws Error if session doesn't exist (session must be created by controller layer)
|
|
1456
1862
|
*/
|
|
1457
|
-
async
|
|
1458
|
-
await sessions.
|
|
1863
|
+
async patchCredentials(data) {
|
|
1864
|
+
await sessions.patchCredentials(this.userId, this.sessionId, data);
|
|
1459
1865
|
}
|
|
1460
1866
|
/**
|
|
1461
1867
|
* Retrieves stored OAuth client information
|
|
1462
1868
|
*/
|
|
1463
1869
|
async clientInformation() {
|
|
1464
|
-
const data = await this.
|
|
1870
|
+
const data = await this.getCredentials();
|
|
1465
1871
|
if (data.clientId && !this._clientId) {
|
|
1466
1872
|
this._clientId = data.clientId;
|
|
1467
1873
|
}
|
|
@@ -1480,7 +1886,7 @@ var StorageOAuthClientProvider = class {
|
|
|
1480
1886
|
* Stores OAuth client information
|
|
1481
1887
|
*/
|
|
1482
1888
|
async saveClientInformation(clientInformation) {
|
|
1483
|
-
await this.
|
|
1889
|
+
await this.patchCredentials({
|
|
1484
1890
|
clientInformation,
|
|
1485
1891
|
clientId: clientInformation.client_id
|
|
1486
1892
|
});
|
|
@@ -1490,29 +1896,58 @@ var StorageOAuthClientProvider = class {
|
|
|
1490
1896
|
* Stores OAuth tokens
|
|
1491
1897
|
*/
|
|
1492
1898
|
async saveTokens(tokens) {
|
|
1493
|
-
|
|
1494
|
-
if (tokens.expires_in) {
|
|
1495
|
-
this.tokenExpiresAt = Date.now() + tokens.expires_in * 1e3 - TOKEN_EXPIRY_BUFFER_MS;
|
|
1496
|
-
}
|
|
1497
|
-
await this.saveSessionData(data);
|
|
1899
|
+
await this.patchCredentials({ tokens });
|
|
1498
1900
|
}
|
|
1499
1901
|
get authUrl() {
|
|
1500
1902
|
return this._authUrl;
|
|
1501
1903
|
}
|
|
1502
1904
|
async state() {
|
|
1503
|
-
|
|
1905
|
+
const nonce = nanoid(32);
|
|
1906
|
+
await this.patchCredentials({
|
|
1907
|
+
oauthState: {
|
|
1908
|
+
nonce,
|
|
1909
|
+
sessionId: this.sessionId,
|
|
1910
|
+
serverId: this.serverId,
|
|
1911
|
+
createdAt: Date.now()
|
|
1912
|
+
},
|
|
1913
|
+
codeVerifier: null
|
|
1914
|
+
});
|
|
1915
|
+
return formatOAuthState(nonce, this.sessionId);
|
|
1504
1916
|
}
|
|
1505
|
-
async checkState(
|
|
1506
|
-
const
|
|
1917
|
+
async checkState(state) {
|
|
1918
|
+
const parsed = parseOAuthState(state);
|
|
1919
|
+
if (!parsed) {
|
|
1920
|
+
return { valid: false, error: "Invalid OAuth state" };
|
|
1921
|
+
}
|
|
1922
|
+
if (parsed.sessionId !== this.sessionId) {
|
|
1923
|
+
return { valid: false, error: "OAuth state mismatch" };
|
|
1924
|
+
}
|
|
1925
|
+
const data = await sessions.getCredentials(this.userId, parsed.sessionId);
|
|
1507
1926
|
if (!data) {
|
|
1508
1927
|
return { valid: false, error: "Session not found" };
|
|
1509
1928
|
}
|
|
1510
|
-
|
|
1929
|
+
const oauthState = data.oauthState;
|
|
1930
|
+
if (!oauthState) {
|
|
1931
|
+
return { valid: false, error: "OAuth state not found" };
|
|
1932
|
+
}
|
|
1933
|
+
if (oauthState.nonce !== parsed.nonce || oauthState.sessionId !== parsed.sessionId || oauthState.serverId !== this.serverId) {
|
|
1934
|
+
return { valid: false, error: "OAuth state mismatch" };
|
|
1935
|
+
}
|
|
1936
|
+
if (Date.now() - oauthState.createdAt > STATE_EXPIRATION_MS) {
|
|
1937
|
+
return { valid: false, error: "OAuth state expired" };
|
|
1938
|
+
}
|
|
1939
|
+
return { valid: true, serverId: oauthState.serverId };
|
|
1511
1940
|
}
|
|
1512
|
-
async consumeState(
|
|
1941
|
+
async consumeState(state) {
|
|
1942
|
+
const result = await this.checkState(state);
|
|
1943
|
+
if (!result.valid) {
|
|
1944
|
+
throw new Error(result.error || "Invalid OAuth state");
|
|
1945
|
+
}
|
|
1946
|
+
await this.patchCredentials({ oauthState: null });
|
|
1513
1947
|
}
|
|
1514
1948
|
async redirectToAuthorization(authUrl) {
|
|
1515
1949
|
this._authUrl = authUrl.toString();
|
|
1950
|
+
await sessions.update(this.userId, this.sessionId, { authUrl: this._authUrl });
|
|
1516
1951
|
if (this.onRedirectCallback) {
|
|
1517
1952
|
this.onRedirectCallback(authUrl.toString());
|
|
1518
1953
|
}
|
|
@@ -1523,21 +1958,25 @@ var StorageOAuthClientProvider = class {
|
|
|
1523
1958
|
} else {
|
|
1524
1959
|
const updates = {};
|
|
1525
1960
|
if (scope === "client") {
|
|
1526
|
-
updates.clientInformation =
|
|
1527
|
-
updates.clientId =
|
|
1961
|
+
updates.clientInformation = null;
|
|
1962
|
+
updates.clientId = null;
|
|
1528
1963
|
} else if (scope === "tokens") {
|
|
1529
|
-
updates.tokens =
|
|
1964
|
+
updates.tokens = null;
|
|
1530
1965
|
} else if (scope === "verifier") {
|
|
1531
|
-
updates.codeVerifier =
|
|
1966
|
+
updates.codeVerifier = null;
|
|
1532
1967
|
}
|
|
1533
|
-
await this.
|
|
1968
|
+
await this.patchCredentials(updates);
|
|
1534
1969
|
}
|
|
1535
1970
|
}
|
|
1536
1971
|
async saveCodeVerifier(verifier) {
|
|
1537
|
-
await this.
|
|
1972
|
+
const data = await this.getCredentials();
|
|
1973
|
+
if (data.codeVerifier) {
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
await this.patchCredentials({ codeVerifier: verifier });
|
|
1538
1977
|
}
|
|
1539
1978
|
async codeVerifier() {
|
|
1540
|
-
const data = await this.
|
|
1979
|
+
const data = await this.getCredentials();
|
|
1541
1980
|
if (data.clientId && !this._clientId) {
|
|
1542
1981
|
this._clientId = data.clientId;
|
|
1543
1982
|
}
|
|
@@ -1547,23 +1986,14 @@ var StorageOAuthClientProvider = class {
|
|
|
1547
1986
|
return data.codeVerifier;
|
|
1548
1987
|
}
|
|
1549
1988
|
async deleteCodeVerifier() {
|
|
1550
|
-
await this.
|
|
1989
|
+
await this.patchCredentials({ codeVerifier: null });
|
|
1551
1990
|
}
|
|
1552
1991
|
async tokens() {
|
|
1553
|
-
const data = await this.
|
|
1992
|
+
const data = await this.getCredentials();
|
|
1554
1993
|
if (data.clientId && !this._clientId) {
|
|
1555
1994
|
this._clientId = data.clientId;
|
|
1556
1995
|
}
|
|
1557
|
-
return data.tokens;
|
|
1558
|
-
}
|
|
1559
|
-
isTokenExpired() {
|
|
1560
|
-
if (!this.tokenExpiresAt) {
|
|
1561
|
-
return false;
|
|
1562
|
-
}
|
|
1563
|
-
return Date.now() >= this.tokenExpiresAt;
|
|
1564
|
-
}
|
|
1565
|
-
setTokenExpiresAt(expiresAt) {
|
|
1566
|
-
this.tokenExpiresAt = expiresAt;
|
|
1996
|
+
return data.tokens ?? void 0;
|
|
1567
1997
|
}
|
|
1568
1998
|
};
|
|
1569
1999
|
|
|
@@ -1788,7 +2218,7 @@ var MCPClient = class {
|
|
|
1788
2218
|
* Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
|
|
1789
2219
|
* This wrapper enforces a timeout and properly uses AbortController to unblock the request.
|
|
1790
2220
|
*/
|
|
1791
|
-
fetch: (url, init) => {
|
|
2221
|
+
fetch: async (url, init) => {
|
|
1792
2222
|
const timeout = 3e4;
|
|
1793
2223
|
const controller = new AbortController();
|
|
1794
2224
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
@@ -1796,7 +2226,17 @@ var MCPClient = class {
|
|
|
1796
2226
|
// @ts-ignore: AbortSignal.any is available in Node 20+
|
|
1797
2227
|
AbortSignal.any ? AbortSignal.any([init.signal, controller.signal]) : controller.signal
|
|
1798
2228
|
) : controller.signal;
|
|
1799
|
-
|
|
2229
|
+
try {
|
|
2230
|
+
const response = await fetch(url, { ...init, signal });
|
|
2231
|
+
const hasSessionHeader = init?.headers && new Headers(init.headers).has("mcp-session-id");
|
|
2232
|
+
if (response.status === 404 && hasSessionHeader) {
|
|
2233
|
+
this.client = null;
|
|
2234
|
+
throw new Error("MCP_SESSION_EXPIRED: Downstream session was not found on the server.");
|
|
2235
|
+
}
|
|
2236
|
+
return response;
|
|
2237
|
+
} finally {
|
|
2238
|
+
clearTimeout(timeoutId);
|
|
2239
|
+
}
|
|
1800
2240
|
}
|
|
1801
2241
|
};
|
|
1802
2242
|
if (type === "sse") {
|
|
@@ -1817,17 +2257,18 @@ var MCPClient = class {
|
|
|
1817
2257
|
}
|
|
1818
2258
|
this.emitStateChange("INITIALIZING");
|
|
1819
2259
|
this.emitProgress("Loading session configuration...");
|
|
2260
|
+
let existingSession = null;
|
|
1820
2261
|
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
1821
|
-
|
|
1822
|
-
if (!
|
|
2262
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2263
|
+
if (!existingSession) {
|
|
1823
2264
|
throw new Error(`Session not found: ${this.sessionId}`);
|
|
1824
2265
|
}
|
|
1825
|
-
this.serverUrl = this.serverUrl ||
|
|
1826
|
-
this.callbackUrl = this.callbackUrl ||
|
|
1827
|
-
this.serverName = this.serverName ||
|
|
1828
|
-
this.serverId = this.serverId ||
|
|
1829
|
-
this.headers = this.headers ||
|
|
1830
|
-
this.createdAt =
|
|
2266
|
+
this.serverUrl = this.serverUrl || existingSession.serverUrl;
|
|
2267
|
+
this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
|
|
2268
|
+
this.serverName = this.serverName || existingSession.serverName;
|
|
2269
|
+
this.serverId = this.serverId || existingSession.serverId || "unknown";
|
|
2270
|
+
this.headers = this.headers || existingSession.headers;
|
|
2271
|
+
this.createdAt = existingSession.createdAt;
|
|
1831
2272
|
}
|
|
1832
2273
|
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
1833
2274
|
throw new Error("Missing required connection metadata");
|
|
@@ -1871,10 +2312,13 @@ var MCPClient = class {
|
|
|
1871
2312
|
}
|
|
1872
2313
|
);
|
|
1873
2314
|
}
|
|
1874
|
-
|
|
2315
|
+
if (existingSession === null) {
|
|
2316
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2317
|
+
}
|
|
1875
2318
|
if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
|
|
1876
2319
|
this.createdAt = Date.now();
|
|
1877
|
-
|
|
2320
|
+
const updatedAt = this.createdAt;
|
|
2321
|
+
console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
|
|
1878
2322
|
await sessions.create({
|
|
1879
2323
|
sessionId: this.sessionId,
|
|
1880
2324
|
userId: this.userId,
|
|
@@ -1885,18 +2329,18 @@ var MCPClient = class {
|
|
|
1885
2329
|
transportType: this.transportType || "streamable-http",
|
|
1886
2330
|
headers: this.headers,
|
|
1887
2331
|
createdAt: this.createdAt,
|
|
1888
|
-
|
|
1889
|
-
|
|
2332
|
+
updatedAt,
|
|
2333
|
+
status: "pending"
|
|
2334
|
+
});
|
|
1890
2335
|
}
|
|
1891
2336
|
}
|
|
1892
2337
|
/**
|
|
1893
2338
|
* Saves current session state to the session store
|
|
1894
2339
|
* Creates new session if it doesn't exist, updates if it does
|
|
1895
|
-
* @param
|
|
1896
|
-
* @param active - Session status marker used to avoid unnecessary TTL rewrites
|
|
2340
|
+
* @param status - Session lifecycle status used by storage cleanup
|
|
1897
2341
|
* @private
|
|
1898
2342
|
*/
|
|
1899
|
-
async saveSession(
|
|
2343
|
+
async saveSession(status = "active", existingSession) {
|
|
1900
2344
|
if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
|
|
1901
2345
|
return;
|
|
1902
2346
|
}
|
|
@@ -1910,13 +2354,29 @@ var MCPClient = class {
|
|
|
1910
2354
|
transportType: this.transportType || "streamable-http",
|
|
1911
2355
|
headers: this.headers,
|
|
1912
2356
|
createdAt: this.createdAt || Date.now(),
|
|
1913
|
-
|
|
2357
|
+
updatedAt: Date.now(),
|
|
2358
|
+
status
|
|
1914
2359
|
};
|
|
1915
|
-
|
|
2360
|
+
if (status === "active") {
|
|
2361
|
+
sessionData.authUrl = null;
|
|
2362
|
+
}
|
|
2363
|
+
if (existingSession === void 0) {
|
|
2364
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2365
|
+
}
|
|
1916
2366
|
if (existingSession) {
|
|
1917
|
-
await sessions.update(this.userId, this.sessionId, sessionData
|
|
2367
|
+
await sessions.update(this.userId, this.sessionId, sessionData);
|
|
1918
2368
|
} else {
|
|
1919
|
-
await sessions.create(sessionData
|
|
2369
|
+
await sessions.create(sessionData);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
/**
|
|
2373
|
+
* Removes transient setup/auth sessions without masking the original error.
|
|
2374
|
+
* @private
|
|
2375
|
+
*/
|
|
2376
|
+
async deleteTransientSession() {
|
|
2377
|
+
try {
|
|
2378
|
+
await sessions.delete(this.userId, this.sessionId);
|
|
2379
|
+
} catch {
|
|
1920
2380
|
}
|
|
1921
2381
|
}
|
|
1922
2382
|
/**
|
|
@@ -1968,6 +2428,14 @@ var MCPClient = class {
|
|
|
1968
2428
|
* @throws {Error} When connection fails for other reasons
|
|
1969
2429
|
*/
|
|
1970
2430
|
async connect() {
|
|
2431
|
+
if (this.client?.transport) {
|
|
2432
|
+
this.transport = null;
|
|
2433
|
+
try {
|
|
2434
|
+
await this.client.close();
|
|
2435
|
+
} catch {
|
|
2436
|
+
}
|
|
2437
|
+
this.client = null;
|
|
2438
|
+
}
|
|
1971
2439
|
await this.initialize();
|
|
1972
2440
|
if (!this.client || !this.oauthProvider) {
|
|
1973
2441
|
const error = "Client or OAuth provider not initialized";
|
|
@@ -1981,8 +2449,8 @@ var MCPClient = class {
|
|
|
1981
2449
|
this.transportType = transportType;
|
|
1982
2450
|
this.emitStateChange("CONNECTED");
|
|
1983
2451
|
this.emitProgress("Connected successfully");
|
|
1984
|
-
console.log(`[MCPClient] Saving session ${this.sessionId}
|
|
1985
|
-
await this.saveSession(
|
|
2452
|
+
console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
|
|
2453
|
+
await this.saveSession("active");
|
|
1986
2454
|
} catch (error) {
|
|
1987
2455
|
if (error instanceof UnauthorizedError$1 || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
|
|
1988
2456
|
let authUrl = "";
|
|
@@ -1994,15 +2462,12 @@ var MCPClient = class {
|
|
|
1994
2462
|
const message = detail.toLowerCase() === "unauthorized" ? "OAuth authorization URL not available" : `OAuth authorization URL not available: ${detail}`;
|
|
1995
2463
|
this.emitError(message, "auth");
|
|
1996
2464
|
this.emitStateChange("FAILED");
|
|
1997
|
-
|
|
1998
|
-
await sessions.delete(this.userId, this.sessionId);
|
|
1999
|
-
} catch {
|
|
2000
|
-
}
|
|
2465
|
+
await this.deleteTransientSession();
|
|
2001
2466
|
throw new Error(message);
|
|
2002
2467
|
}
|
|
2003
2468
|
this.emitStateChange("AUTHENTICATING");
|
|
2004
|
-
console.log(`[MCPClient] Saving session ${this.sessionId}
|
|
2005
|
-
await this.saveSession(
|
|
2469
|
+
console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
|
|
2470
|
+
await this.saveSession("pending");
|
|
2006
2471
|
if (this.serverId) {
|
|
2007
2472
|
this._onConnectionEvent.fire({
|
|
2008
2473
|
type: "auth_required",
|
|
@@ -2022,7 +2487,7 @@ var MCPClient = class {
|
|
|
2022
2487
|
this.emitStateChange("FAILED");
|
|
2023
2488
|
try {
|
|
2024
2489
|
const existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2025
|
-
if (!existingSession || existingSession.
|
|
2490
|
+
if (!existingSession || existingSession.status !== "active") {
|
|
2026
2491
|
await sessions.delete(this.userId, this.sessionId);
|
|
2027
2492
|
}
|
|
2028
2493
|
} catch {
|
|
@@ -2037,7 +2502,7 @@ var MCPClient = class {
|
|
|
2037
2502
|
* @param authCode - Authorization code received from OAuth callback
|
|
2038
2503
|
*/
|
|
2039
2504
|
// TODO: needs to be optimized
|
|
2040
|
-
async finishAuth(authCode) {
|
|
2505
|
+
async finishAuth(authCode, state) {
|
|
2041
2506
|
this.emitStateChange("AUTHENTICATING");
|
|
2042
2507
|
this.emitProgress("Exchanging authorization code for tokens...");
|
|
2043
2508
|
await this.initialize();
|
|
@@ -2047,6 +2512,16 @@ var MCPClient = class {
|
|
|
2047
2512
|
this.emitStateChange("FAILED");
|
|
2048
2513
|
throw new Error(error);
|
|
2049
2514
|
}
|
|
2515
|
+
if (state) {
|
|
2516
|
+
const stateCheck = await this.oauthProvider.checkState(state);
|
|
2517
|
+
if (!stateCheck.valid) {
|
|
2518
|
+
const error = stateCheck.error || "Invalid OAuth state";
|
|
2519
|
+
this.emitError(error, "auth");
|
|
2520
|
+
this.emitStateChange("FAILED");
|
|
2521
|
+
throw new Error(error);
|
|
2522
|
+
}
|
|
2523
|
+
await this.oauthProvider.consumeState(state);
|
|
2524
|
+
}
|
|
2050
2525
|
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2051
2526
|
let lastError;
|
|
2052
2527
|
let tokensExchanged = false;
|
|
@@ -2086,8 +2561,8 @@ var MCPClient = class {
|
|
|
2086
2561
|
await this.client.connect(this.transport);
|
|
2087
2562
|
this.transportType = currentType;
|
|
2088
2563
|
this.emitStateChange("CONNECTED");
|
|
2089
|
-
console.log(`[MCPClient]
|
|
2090
|
-
await this.saveSession(
|
|
2564
|
+
console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
|
|
2565
|
+
await this.saveSession("active");
|
|
2091
2566
|
return;
|
|
2092
2567
|
} catch (error) {
|
|
2093
2568
|
lastError = error;
|
|
@@ -2100,12 +2575,14 @@ var MCPClient = class {
|
|
|
2100
2575
|
const msg = error instanceof Error ? error.message : "Authentication failed";
|
|
2101
2576
|
this.emitError(msg, "auth");
|
|
2102
2577
|
this.emitStateChange("FAILED");
|
|
2578
|
+
await this.deleteTransientSession();
|
|
2103
2579
|
throw error;
|
|
2104
2580
|
}
|
|
2105
2581
|
if (isLastAttempt) {
|
|
2106
2582
|
const msg = error instanceof Error ? error.message : "Authentication failed";
|
|
2107
2583
|
this.emitError(msg, "auth");
|
|
2108
2584
|
this.emitStateChange("FAILED");
|
|
2585
|
+
await this.deleteTransientSession();
|
|
2109
2586
|
throw error;
|
|
2110
2587
|
}
|
|
2111
2588
|
this.emitProgress(`Auth attempt with ${currentType} failed: ${errorMessage}. Retrying...`);
|
|
@@ -2115,6 +2592,7 @@ var MCPClient = class {
|
|
|
2115
2592
|
const errorMessage = lastError instanceof Error ? lastError.message : "Authentication failed";
|
|
2116
2593
|
this.emitError(errorMessage, "auth");
|
|
2117
2594
|
this.emitStateChange("FAILED");
|
|
2595
|
+
await this.deleteTransientSession();
|
|
2118
2596
|
throw lastError;
|
|
2119
2597
|
}
|
|
2120
2598
|
}
|
|
@@ -2336,7 +2814,7 @@ var MCPClient = class {
|
|
|
2336
2814
|
await this.oauthProvider.invalidateCredentials("all");
|
|
2337
2815
|
}
|
|
2338
2816
|
await sessions.delete(this.userId, this.sessionId);
|
|
2339
|
-
this.disconnect();
|
|
2817
|
+
await this.disconnect();
|
|
2340
2818
|
}
|
|
2341
2819
|
/**
|
|
2342
2820
|
* Checks if the client is currently connected to an MCP server
|
|
@@ -2346,10 +2824,21 @@ var MCPClient = class {
|
|
|
2346
2824
|
return this.client !== null;
|
|
2347
2825
|
}
|
|
2348
2826
|
/**
|
|
2349
|
-
* Disconnects from the MCP server and cleans up resources
|
|
2350
|
-
* Does not remove session from Redis
|
|
2827
|
+
* Disconnects from the MCP server and cleans up resources.
|
|
2828
|
+
* Does not remove session from Redis — use clearSession() for that.
|
|
2829
|
+
*
|
|
2830
|
+
* For Streamable HTTP sessions, sends an HTTP DELETE to the MCP endpoint
|
|
2831
|
+
* before closing, as recommended by the MCP Streamable HTTP spec
|
|
2832
|
+
* (section "Session Management", rule 5). This is best-effort — errors
|
|
2833
|
+
* (e.g. server already restarted, 404/405 responses) are silently ignored.
|
|
2351
2834
|
*/
|
|
2352
|
-
disconnect(
|
|
2835
|
+
async disconnect() {
|
|
2836
|
+
if (this.transport instanceof StreamableHTTPClientTransport) {
|
|
2837
|
+
try {
|
|
2838
|
+
await this.transport.terminateSession();
|
|
2839
|
+
} catch {
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2353
2842
|
if (this.client) {
|
|
2354
2843
|
this.client.close();
|
|
2355
2844
|
}
|
|
@@ -2361,7 +2850,6 @@ var MCPClient = class {
|
|
|
2361
2850
|
type: "disconnected",
|
|
2362
2851
|
sessionId: this.sessionId,
|
|
2363
2852
|
serverId: this.serverId,
|
|
2364
|
-
reason,
|
|
2365
2853
|
timestamp: Date.now()
|
|
2366
2854
|
});
|
|
2367
2855
|
this._onObservabilityEvent.fire({
|
|
@@ -2370,9 +2858,7 @@ var MCPClient = class {
|
|
|
2370
2858
|
message: `Disconnected from ${this.serverId}`,
|
|
2371
2859
|
sessionId: this.sessionId,
|
|
2372
2860
|
serverId: this.serverId,
|
|
2373
|
-
payload: {
|
|
2374
|
-
reason: reason || "unknown"
|
|
2375
|
-
},
|
|
2861
|
+
payload: {},
|
|
2376
2862
|
timestamp: Date.now(),
|
|
2377
2863
|
id: nanoid()
|
|
2378
2864
|
});
|
|
@@ -2429,48 +2915,6 @@ var MCPClient = class {
|
|
|
2429
2915
|
getSessionId() {
|
|
2430
2916
|
return this.sessionId;
|
|
2431
2917
|
}
|
|
2432
|
-
/**
|
|
2433
|
-
* Gets MCP server configuration for all active user sessions
|
|
2434
|
-
* Loads sessions from storage and returns server connection metadata.
|
|
2435
|
-
* OAuth refresh is handled by SDK transports through their authProvider.
|
|
2436
|
-
* @deprecated This returns legacy connection metadata only and does not
|
|
2437
|
-
* include OAuth tokens or generated Authorization headers. Prefer
|
|
2438
|
-
* MultiSessionClient or explicit MCPClient instances so SDK transports can
|
|
2439
|
-
* own OAuth refresh and reauthorization.
|
|
2440
|
-
* @param userId - User ID to fetch sessions for
|
|
2441
|
-
* @returns Object keyed by sanitized server labels containing transport and url.
|
|
2442
|
-
* @static
|
|
2443
|
-
*/
|
|
2444
|
-
static async getMcpServerConfig(userId) {
|
|
2445
|
-
const mcpConfig = {};
|
|
2446
|
-
const sessionList = await sessions.list(userId);
|
|
2447
|
-
await Promise.all(
|
|
2448
|
-
sessionList.map(async (sessionData) => {
|
|
2449
|
-
const { sessionId } = sessionData;
|
|
2450
|
-
try {
|
|
2451
|
-
if (!sessionData.serverId || !sessionData.transportType || !sessionData.serverUrl || !sessionData.callbackUrl) {
|
|
2452
|
-
await sessions.delete(userId, sessionId);
|
|
2453
|
-
return;
|
|
2454
|
-
}
|
|
2455
|
-
const label = sanitizeServerLabel(
|
|
2456
|
-
sessionData.serverName || sessionData.serverId || "server"
|
|
2457
|
-
);
|
|
2458
|
-
mcpConfig[label] = {
|
|
2459
|
-
transport: sessionData.transportType,
|
|
2460
|
-
url: sessionData.serverUrl,
|
|
2461
|
-
...sessionData.serverName && {
|
|
2462
|
-
serverName: sessionData.serverName,
|
|
2463
|
-
serverLabel: label
|
|
2464
|
-
}
|
|
2465
|
-
};
|
|
2466
|
-
} catch (error) {
|
|
2467
|
-
await sessions.delete(userId, sessionId);
|
|
2468
|
-
console.warn(`[MCP] Failed to process session ${sessionId}:`, error);
|
|
2469
|
-
}
|
|
2470
|
-
})
|
|
2471
|
-
);
|
|
2472
|
-
return mcpConfig;
|
|
2473
|
-
}
|
|
2474
2918
|
};
|
|
2475
2919
|
|
|
2476
2920
|
// src/server/mcp/multi-session-client.ts
|
|
@@ -2505,18 +2949,13 @@ var MultiSessionClient = class {
|
|
|
2505
2949
|
*
|
|
2506
2950
|
* A session is considered connectable when:
|
|
2507
2951
|
* - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
|
|
2508
|
-
* - Its
|
|
2509
|
-
* either mid-OAuth flow, auth-pending, or previously failed. We skip those here
|
|
2952
|
+
* - Its status is `active`. Pending sessions are skipped here
|
|
2510
2953
|
* and let the OAuth flow complete separately before we try to reconnect them.
|
|
2511
|
-
*
|
|
2512
|
-
* Note: Sessions where `active` is `undefined` (legacy records) are included
|
|
2513
|
-
* for backwards compatibility.
|
|
2514
2954
|
*/
|
|
2515
2955
|
async getActiveSessions() {
|
|
2516
2956
|
const sessionList = await sessions.list(this.userId);
|
|
2517
2957
|
const valid = sessionList.filter(
|
|
2518
|
-
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.
|
|
2519
|
-
// exclude OAuth-pending / failed sessions
|
|
2958
|
+
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
|
|
2520
2959
|
);
|
|
2521
2960
|
return valid;
|
|
2522
2961
|
}
|
|
@@ -2537,6 +2976,9 @@ var MultiSessionClient = class {
|
|
|
2537
2976
|
* Connects a single session, with built-in deduplication to prevent race conditions.
|
|
2538
2977
|
*
|
|
2539
2978
|
* - If a client for this session already exists and is connected, returns immediately.
|
|
2979
|
+
* - If the existing client entry is no longer connected (e.g. it was explicitly
|
|
2980
|
+
* disconnected), it is evicted so that `establishConnectionWithRetries` creates a
|
|
2981
|
+
* fresh transport — preventing "Client already connected" errors from the SDK.
|
|
2540
2982
|
* - If a connection attempt for this session is already in-flight (e.g. from a
|
|
2541
2983
|
* concurrent call), it joins the existing promise instead of starting a new one.
|
|
2542
2984
|
* This is the key concurrency lock — the `connectionPromises` map acts as a
|
|
@@ -2544,9 +2986,12 @@ var MultiSessionClient = class {
|
|
|
2544
2986
|
* - On completion (success or failure), the promise is cleaned up from the map.
|
|
2545
2987
|
*/
|
|
2546
2988
|
async connectSession(session) {
|
|
2547
|
-
const
|
|
2548
|
-
if (
|
|
2549
|
-
|
|
2989
|
+
const existing = this.clients.find((c) => c.getSessionId() === session.sessionId);
|
|
2990
|
+
if (existing) {
|
|
2991
|
+
if (existing.isConnected()) {
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
this.clients = this.clients.filter((c) => c !== existing);
|
|
2550
2995
|
}
|
|
2551
2996
|
if (this.connectionPromises.has(session.sessionId)) {
|
|
2552
2997
|
return this.connectionPromises.get(session.sessionId);
|
|
@@ -2621,8 +3066,24 @@ var MultiSessionClient = class {
|
|
|
2621
3066
|
*/
|
|
2622
3067
|
async connect() {
|
|
2623
3068
|
const sessions2 = await this.getActiveSessions();
|
|
3069
|
+
const activeSessionIds = new Set(sessions2.map((s) => s.sessionId));
|
|
3070
|
+
this.clients = this.clients.filter((c) => activeSessionIds.has(c.getSessionId()));
|
|
2624
3071
|
await this.connectInBatches(sessions2);
|
|
2625
3072
|
}
|
|
3073
|
+
/**
|
|
3074
|
+
* Drops all cached `MCPClient` instances and reconnects fresh from storage.
|
|
3075
|
+
*
|
|
3076
|
+
* Call this when downstream MCP servers have expired their transport sessions
|
|
3077
|
+
* (e.g. after a remote server restart) and subsequent tool calls return
|
|
3078
|
+
* "Session not found. Reconnect without session header." errors.
|
|
3079
|
+
*
|
|
3080
|
+
* OAuth tokens are preserved in the storage backend — no re-authentication
|
|
3081
|
+
* is required. Only the in-memory transport sessions are cleared.
|
|
3082
|
+
*/
|
|
3083
|
+
async reconnect() {
|
|
3084
|
+
await this.disconnect();
|
|
3085
|
+
await this.connect();
|
|
3086
|
+
}
|
|
2626
3087
|
/**
|
|
2627
3088
|
* Returns all currently connected `MCPClient` instances.
|
|
2628
3089
|
*
|
|
@@ -2635,11 +3096,15 @@ var MultiSessionClient = class {
|
|
|
2635
3096
|
/**
|
|
2636
3097
|
* Gracefully disconnects all active MCP clients and clears the internal client list.
|
|
2637
3098
|
*
|
|
3099
|
+
* For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
|
|
3100
|
+
* endpoint per the spec before closing locally. All disconnects run in
|
|
3101
|
+
* parallel so shutdown is not serialised across many sessions.
|
|
3102
|
+
*
|
|
2638
3103
|
* Call this during server shutdown or when a user logs out to free up
|
|
2639
3104
|
* underlying transport resources (SSE streams, HTTP connections, etc.).
|
|
2640
3105
|
*/
|
|
2641
|
-
disconnect() {
|
|
2642
|
-
this.clients.
|
|
3106
|
+
async disconnect() {
|
|
3107
|
+
await Promise.all(this.clients.map((client) => client.disconnect()));
|
|
2643
3108
|
this.clients = [];
|
|
2644
3109
|
}
|
|
2645
3110
|
};
|
|
@@ -2787,7 +3252,8 @@ var SSEConnectionManager = class {
|
|
|
2787
3252
|
serverUrl: s.serverUrl,
|
|
2788
3253
|
transport: s.transportType,
|
|
2789
3254
|
createdAt: s.createdAt,
|
|
2790
|
-
|
|
3255
|
+
updatedAt: s.updatedAt ?? s.createdAt,
|
|
3256
|
+
status: s.status ?? "pending"
|
|
2791
3257
|
}))
|
|
2792
3258
|
};
|
|
2793
3259
|
}
|
|
@@ -2803,7 +3269,7 @@ var SSEConnectionManager = class {
|
|
|
2803
3269
|
(s) => s.serverId === serverId || s.serverUrl === serverUrl
|
|
2804
3270
|
);
|
|
2805
3271
|
if (duplicate) {
|
|
2806
|
-
if (duplicate.
|
|
3272
|
+
if (duplicate.status === "pending") {
|
|
2807
3273
|
await this.getSession({ sessionId: duplicate.sessionId });
|
|
2808
3274
|
return {
|
|
2809
3275
|
sessionId: duplicate.sessionId,
|
|
@@ -2868,7 +3334,6 @@ var SSEConnectionManager = class {
|
|
|
2868
3334
|
const client = this.clients.get(sessionId);
|
|
2869
3335
|
if (client) {
|
|
2870
3336
|
await client.clearSession();
|
|
2871
|
-
client.disconnect();
|
|
2872
3337
|
this.clients.delete(sessionId);
|
|
2873
3338
|
} else {
|
|
2874
3339
|
await sessions.delete(this.userId, sessionId);
|
|
@@ -2990,7 +3455,10 @@ var SSEConnectionManager = class {
|
|
|
2990
3455
|
* Complete OAuth authorization flow
|
|
2991
3456
|
*/
|
|
2992
3457
|
async finishAuth(params) {
|
|
2993
|
-
const {
|
|
3458
|
+
const { code } = params;
|
|
3459
|
+
const oauthState = params.state;
|
|
3460
|
+
const parsedState = parseOAuthState(oauthState);
|
|
3461
|
+
const sessionId = parsedState?.sessionId || oauthState;
|
|
2994
3462
|
const session = await sessions.get(this.userId, sessionId);
|
|
2995
3463
|
if (!session) {
|
|
2996
3464
|
throw new Error("Session not found");
|
|
@@ -3015,7 +3483,7 @@ var SSEConnectionManager = class {
|
|
|
3015
3483
|
headers: session.headers
|
|
3016
3484
|
});
|
|
3017
3485
|
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
3018
|
-
await client.finishAuth(code);
|
|
3486
|
+
await client.finishAuth(code, oauthState);
|
|
3019
3487
|
this.clients.set(sessionId, client);
|
|
3020
3488
|
const tools = await client.listTools();
|
|
3021
3489
|
return { success: true, toolCount: tools.tools.length };
|
|
@@ -3074,14 +3542,14 @@ var SSEConnectionManager = class {
|
|
|
3074
3542
|
/**
|
|
3075
3543
|
* Cleanup and close all connections
|
|
3076
3544
|
*/
|
|
3077
|
-
dispose() {
|
|
3545
|
+
async dispose() {
|
|
3078
3546
|
this.isActive = false;
|
|
3079
3547
|
if (this.heartbeatTimer) {
|
|
3080
3548
|
clearInterval(this.heartbeatTimer);
|
|
3081
3549
|
}
|
|
3082
|
-
|
|
3083
|
-
client.disconnect()
|
|
3084
|
-
|
|
3550
|
+
await Promise.all(
|
|
3551
|
+
Array.from(this.clients.values()).map((client) => client.disconnect())
|
|
3552
|
+
);
|
|
3085
3553
|
this.clients.clear();
|
|
3086
3554
|
}
|
|
3087
3555
|
};
|