@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.js
CHANGED
|
@@ -156,22 +156,9 @@ init_cjs_shims();
|
|
|
156
156
|
// src/server/storage/redis-backend.ts
|
|
157
157
|
init_cjs_shims();
|
|
158
158
|
|
|
159
|
-
// src/shared/constants.ts
|
|
160
|
-
init_cjs_shims();
|
|
161
|
-
var SESSION_TTL_SECONDS = 43200;
|
|
162
|
-
var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
|
|
163
|
-
var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
164
|
-
var DEFAULT_CLIENT_NAME = "MCP Assistant";
|
|
165
|
-
var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
|
|
166
|
-
var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
|
|
167
|
-
var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
|
|
168
|
-
var SOFTWARE_ID = "@mcp-ts";
|
|
169
|
-
var SOFTWARE_VERSION = "1.3.4";
|
|
170
|
-
var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
|
|
171
|
-
var MCP_CLIENT_VERSION = "2.0";
|
|
172
|
-
|
|
173
159
|
// src/shared/utils.ts
|
|
174
160
|
init_cjs_shims();
|
|
161
|
+
var OAUTH_STATE_SEPARATOR = ".";
|
|
175
162
|
var firstChar = nanoid.customAlphabet(
|
|
176
163
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
|
177
164
|
1
|
|
@@ -190,13 +177,99 @@ function sanitizeServerLabel(name) {
|
|
|
190
177
|
function generateSessionId() {
|
|
191
178
|
return firstChar() + rest();
|
|
192
179
|
}
|
|
180
|
+
function formatOAuthState(nonce, sessionId) {
|
|
181
|
+
return `${nonce}${OAUTH_STATE_SEPARATOR}${sessionId}`;
|
|
182
|
+
}
|
|
183
|
+
function parseOAuthState(state) {
|
|
184
|
+
const separatorIndex = state.indexOf(OAUTH_STATE_SEPARATOR);
|
|
185
|
+
if (separatorIndex <= 0 || separatorIndex === state.length - 1) {
|
|
186
|
+
return void 0;
|
|
187
|
+
}
|
|
188
|
+
const nonce = state.slice(0, separatorIndex);
|
|
189
|
+
const sessionId = state.slice(separatorIndex + 1);
|
|
190
|
+
if (!nonce || !sessionId || sessionId.includes(OAUTH_STATE_SEPARATOR)) {
|
|
191
|
+
return void 0;
|
|
192
|
+
}
|
|
193
|
+
return { nonce, sessionId };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/server/storage/session-lifecycle.ts
|
|
197
|
+
init_cjs_shims();
|
|
198
|
+
|
|
199
|
+
// src/shared/constants.ts
|
|
200
|
+
init_cjs_shims();
|
|
201
|
+
var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
|
|
202
|
+
var DORMANT_SESSION_EXPIRATION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
203
|
+
var DORMANT_SESSION_EXPIRATION_SECONDS = Math.floor(DORMANT_SESSION_EXPIRATION_MS / 1e3);
|
|
204
|
+
var DEFAULT_CLIENT_NAME = "MCP Assistant";
|
|
205
|
+
var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
|
|
206
|
+
var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
|
|
207
|
+
var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
|
|
208
|
+
var SOFTWARE_ID = "@mcp-ts";
|
|
209
|
+
var SOFTWARE_VERSION = "2.3.4";
|
|
210
|
+
var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
|
|
211
|
+
var MCP_CLIENT_VERSION = "2.0";
|
|
212
|
+
|
|
213
|
+
// src/server/storage/session-lifecycle.ts
|
|
214
|
+
function resolveSessionExpiresAt(status = "pending", referenceTime = Date.now()) {
|
|
215
|
+
return status === "active" ? null : referenceTime + STATE_EXPIRATION_MS;
|
|
216
|
+
}
|
|
217
|
+
function resolveSessionRedisTtlSeconds(session) {
|
|
218
|
+
return session.status === "active" ? DORMANT_SESSION_EXPIRATION_SECONDS : Math.floor(STATE_EXPIRATION_MS / 1e3);
|
|
219
|
+
}
|
|
220
|
+
function normalizeNewSession(session, now = Date.now()) {
|
|
221
|
+
const createdAt = session.createdAt || now;
|
|
222
|
+
const updatedAt = session.updatedAt ?? createdAt;
|
|
223
|
+
const status = session.status ?? "pending";
|
|
224
|
+
return {
|
|
225
|
+
...session,
|
|
226
|
+
status,
|
|
227
|
+
createdAt,
|
|
228
|
+
updatedAt,
|
|
229
|
+
expiresAt: resolveSessionExpiresAt(status, status === "active" ? updatedAt : createdAt)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function mergeSessionUpdate(current, data, now = Date.now()) {
|
|
233
|
+
const updatedAt = data.updatedAt ?? now;
|
|
234
|
+
const updated = {
|
|
235
|
+
...current,
|
|
236
|
+
...data,
|
|
237
|
+
updatedAt
|
|
238
|
+
};
|
|
239
|
+
const status = updated.status ?? "pending";
|
|
240
|
+
return {
|
|
241
|
+
...updated,
|
|
242
|
+
status,
|
|
243
|
+
expiresAt: resolveSessionExpiresAt(status, updatedAt)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function normalizeStoredSession(session) {
|
|
247
|
+
const createdAt = session.createdAt || Date.now();
|
|
248
|
+
const updatedAt = session.updatedAt ?? createdAt;
|
|
249
|
+
const status = session.status ?? "pending";
|
|
250
|
+
const expiresAt = status === "active" ? null : session.expiresAt ?? resolveSessionExpiresAt(status, createdAt);
|
|
251
|
+
return {
|
|
252
|
+
...session,
|
|
253
|
+
status,
|
|
254
|
+
createdAt,
|
|
255
|
+
updatedAt,
|
|
256
|
+
expiresAt
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function isSessionExpired(session, now = Date.now()) {
|
|
260
|
+
const hydrated = normalizeStoredSession(session);
|
|
261
|
+
if (hydrated.status === "active") {
|
|
262
|
+
return hydrated.updatedAt !== void 0 && hydrated.updatedAt < now - DORMANT_SESSION_EXPIRATION_MS;
|
|
263
|
+
}
|
|
264
|
+
return typeof hydrated.expiresAt === "number" && hydrated.expiresAt < now;
|
|
265
|
+
}
|
|
193
266
|
|
|
194
267
|
// src/server/storage/redis-backend.ts
|
|
195
268
|
var RedisStorageBackend = class {
|
|
196
269
|
constructor(redis2) {
|
|
197
270
|
this.redis = redis2;
|
|
198
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
199
271
|
__publicField(this, "KEY_PREFIX", "mcp:session:");
|
|
272
|
+
__publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
|
|
200
273
|
__publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
|
|
201
274
|
__publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
|
|
202
275
|
}
|
|
@@ -215,6 +288,9 @@ var RedisStorageBackend = class {
|
|
|
215
288
|
getSessionKey(userId, sessionId) {
|
|
216
289
|
return `${this.KEY_PREFIX}${userId}:${sessionId}`;
|
|
217
290
|
}
|
|
291
|
+
getCredentialsKey(userId, sessionId) {
|
|
292
|
+
return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
|
|
293
|
+
}
|
|
218
294
|
/**
|
|
219
295
|
* Generates Redis key for tracking all sessions for a user
|
|
220
296
|
* @private
|
|
@@ -252,15 +328,16 @@ var RedisStorageBackend = class {
|
|
|
252
328
|
generateSessionId() {
|
|
253
329
|
return generateSessionId();
|
|
254
330
|
}
|
|
255
|
-
async create(session
|
|
331
|
+
async create(session) {
|
|
256
332
|
const { sessionId, userId } = session;
|
|
257
333
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
258
334
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
259
335
|
const userIdKey = this.getUserIdKey(userId);
|
|
260
|
-
const
|
|
336
|
+
const sessionWithLifecycle = normalizeNewSession(session);
|
|
337
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(sessionWithLifecycle);
|
|
261
338
|
const result = await this.redis.set(
|
|
262
339
|
sessionKey,
|
|
263
|
-
JSON.stringify(
|
|
340
|
+
JSON.stringify(sessionWithLifecycle),
|
|
264
341
|
"EX",
|
|
265
342
|
effectiveTtl,
|
|
266
343
|
"NX"
|
|
@@ -270,9 +347,8 @@ var RedisStorageBackend = class {
|
|
|
270
347
|
}
|
|
271
348
|
await this.redis.sadd(userIdKey, sessionId);
|
|
272
349
|
}
|
|
273
|
-
async update(userId, sessionId, data
|
|
350
|
+
async update(userId, sessionId, data) {
|
|
274
351
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
275
|
-
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
276
352
|
const script = `
|
|
277
353
|
local currentStr = redis.call("GET", KEYS[1])
|
|
278
354
|
if not currentStr then
|
|
@@ -280,26 +356,47 @@ var RedisStorageBackend = class {
|
|
|
280
356
|
end
|
|
281
357
|
|
|
282
358
|
local current = cjson.decode(currentStr)
|
|
283
|
-
local
|
|
284
|
-
|
|
285
|
-
for k,v in pairs(updates) do
|
|
286
|
-
current[k] = v
|
|
287
|
-
end
|
|
359
|
+
local updated = cjson.decode(ARGV[1])
|
|
288
360
|
|
|
289
|
-
redis.call("SET", KEYS[1], cjson.encode(
|
|
361
|
+
redis.call("SET", KEYS[1], cjson.encode(updated), "EX", ARGV[2])
|
|
290
362
|
return 1
|
|
291
363
|
`;
|
|
364
|
+
const current = await this.get(userId, sessionId);
|
|
365
|
+
if (!current) {
|
|
366
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
367
|
+
}
|
|
368
|
+
const updated = mergeSessionUpdate(current, data);
|
|
369
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
|
|
292
370
|
const result = await this.redis.eval(
|
|
293
371
|
script,
|
|
294
372
|
1,
|
|
295
373
|
sessionKey,
|
|
296
|
-
JSON.stringify(
|
|
374
|
+
JSON.stringify(updated),
|
|
297
375
|
effectiveTtl
|
|
298
376
|
);
|
|
299
377
|
if (result === 0) {
|
|
300
378
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
301
379
|
}
|
|
302
380
|
}
|
|
381
|
+
async patchCredentials(userId, sessionId, data) {
|
|
382
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
383
|
+
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
384
|
+
const sessionExists = await this.redis.exists(sessionKey);
|
|
385
|
+
if (!sessionExists) {
|
|
386
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
387
|
+
}
|
|
388
|
+
const session = await this.get(userId, sessionId);
|
|
389
|
+
const currentTtl = await this.redis.ttl(sessionKey);
|
|
390
|
+
const effectiveTtl = currentTtl > 0 && session ? currentTtl : resolveSessionRedisTtlSeconds(session ?? { status: "pending" });
|
|
391
|
+
const currentStr = await this.redis.get(credentialsKey);
|
|
392
|
+
const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
|
|
393
|
+
const credentials = { ...current, ...data, sessionId, userId };
|
|
394
|
+
if (effectiveTtl > 0) {
|
|
395
|
+
await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
|
|
396
|
+
} else {
|
|
397
|
+
await this.redis.set(credentialsKey, JSON.stringify(credentials));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
303
400
|
async get(userId, sessionId) {
|
|
304
401
|
try {
|
|
305
402
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
@@ -307,13 +404,28 @@ var RedisStorageBackend = class {
|
|
|
307
404
|
if (!sessionDataStr) {
|
|
308
405
|
return null;
|
|
309
406
|
}
|
|
310
|
-
const
|
|
311
|
-
return
|
|
407
|
+
const session = JSON.parse(sessionDataStr);
|
|
408
|
+
return normalizeStoredSession(session);
|
|
312
409
|
} catch (error) {
|
|
313
410
|
console.error("[RedisStorageBackend] Failed to get session:", error);
|
|
314
411
|
return null;
|
|
315
412
|
}
|
|
316
413
|
}
|
|
414
|
+
async getCredentials(userId, sessionId) {
|
|
415
|
+
const session = await this.get(userId, sessionId);
|
|
416
|
+
if (!session) return null;
|
|
417
|
+
const credentialsStr = await this.redis.get(this.getCredentialsKey(userId, sessionId));
|
|
418
|
+
return credentialsStr ? JSON.parse(credentialsStr) : { sessionId, userId };
|
|
419
|
+
}
|
|
420
|
+
async clearCredentials(userId, sessionId) {
|
|
421
|
+
await this.patchCredentials(userId, sessionId, {
|
|
422
|
+
clientInformation: null,
|
|
423
|
+
tokens: null,
|
|
424
|
+
codeVerifier: null,
|
|
425
|
+
clientId: null,
|
|
426
|
+
oauthState: null
|
|
427
|
+
});
|
|
428
|
+
}
|
|
317
429
|
async listIds(userId) {
|
|
318
430
|
const sessions2 = await this.list(userId);
|
|
319
431
|
return sessions2.map((session) => session.sessionId);
|
|
@@ -333,7 +445,7 @@ var RedisStorageBackend = class {
|
|
|
333
445
|
if (staleSessionIds.length > 0) {
|
|
334
446
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
335
447
|
}
|
|
336
|
-
return results.filter((session) => session !== null);
|
|
448
|
+
return results.filter((session) => session !== null).map((session) => normalizeStoredSession(session));
|
|
337
449
|
} catch (error) {
|
|
338
450
|
console.error(`[RedisStorageBackend] Failed to get session data for ${userId}:`, error);
|
|
339
451
|
return [];
|
|
@@ -342,9 +454,10 @@ var RedisStorageBackend = class {
|
|
|
342
454
|
async delete(userId, sessionId) {
|
|
343
455
|
try {
|
|
344
456
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
457
|
+
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
345
458
|
const userIdKey = this.getUserIdKey(userId);
|
|
346
459
|
await this.redis.srem(userIdKey, sessionId);
|
|
347
|
-
await this.redis.del(sessionKey);
|
|
460
|
+
await this.redis.del(sessionKey, credentialsKey);
|
|
348
461
|
} catch (error) {
|
|
349
462
|
console.error("[RedisStorageBackend] Failed to remove session:", error);
|
|
350
463
|
}
|
|
@@ -359,7 +472,7 @@ var RedisStorageBackend = class {
|
|
|
359
472
|
return null;
|
|
360
473
|
}
|
|
361
474
|
try {
|
|
362
|
-
return JSON.parse(data).sessionId;
|
|
475
|
+
return normalizeStoredSession(JSON.parse(data)).sessionId;
|
|
363
476
|
} catch (error) {
|
|
364
477
|
console.error("[RedisStorageBackend] Failed to parse session while listing all session IDs:", error);
|
|
365
478
|
return null;
|
|
@@ -375,8 +488,9 @@ var RedisStorageBackend = class {
|
|
|
375
488
|
async clearAll() {
|
|
376
489
|
try {
|
|
377
490
|
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
491
|
+
const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
|
|
378
492
|
const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
|
|
379
|
-
const allKeys = [...keys, ...userIdKeys];
|
|
493
|
+
const allKeys = [...keys, ...credentialKeys, ...userIdKeys];
|
|
380
494
|
if (allKeys.length > 0) {
|
|
381
495
|
await this.redis.del(...allKeys);
|
|
382
496
|
}
|
|
@@ -400,6 +514,7 @@ var RedisStorageBackend = class {
|
|
|
400
514
|
const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
|
|
401
515
|
if (staleSessionIds.length > 0) {
|
|
402
516
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
517
|
+
await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
|
|
403
518
|
}
|
|
404
519
|
const remainingCount = await this.redis.scard(userIdKey);
|
|
405
520
|
if (remainingCount === 0) {
|
|
@@ -425,6 +540,7 @@ var MemoryStorageBackend = class {
|
|
|
425
540
|
constructor() {
|
|
426
541
|
// Map<userId:sessionId, Session>
|
|
427
542
|
__publicField(this, "sessions", /* @__PURE__ */ new Map());
|
|
543
|
+
__publicField(this, "credentials", /* @__PURE__ */ new Map());
|
|
428
544
|
// Map<userId, Set<sessionId>>
|
|
429
545
|
__publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
|
|
430
546
|
}
|
|
@@ -437,36 +553,55 @@ var MemoryStorageBackend = class {
|
|
|
437
553
|
generateSessionId() {
|
|
438
554
|
return generateSessionId();
|
|
439
555
|
}
|
|
440
|
-
async create(session
|
|
556
|
+
async create(session) {
|
|
441
557
|
const { sessionId, userId } = session;
|
|
442
558
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
443
559
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
444
560
|
if (this.sessions.has(sessionKey)) {
|
|
445
561
|
throw new Error(`Session ${sessionId} already exists`);
|
|
446
562
|
}
|
|
447
|
-
this.sessions.set(sessionKey, session);
|
|
563
|
+
this.sessions.set(sessionKey, normalizeNewSession(session));
|
|
448
564
|
if (!this.userIdSessions.has(userId)) {
|
|
449
565
|
this.userIdSessions.set(userId, /* @__PURE__ */ new Set());
|
|
450
566
|
}
|
|
451
567
|
this.userIdSessions.get(userId).add(sessionId);
|
|
452
568
|
}
|
|
453
|
-
async update(userId, sessionId, data
|
|
569
|
+
async update(userId, sessionId, data) {
|
|
454
570
|
if (!userId || !sessionId) throw new Error("userId and sessionId required");
|
|
455
571
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
456
572
|
const current = this.sessions.get(sessionKey);
|
|
457
573
|
if (!current) {
|
|
458
574
|
throw new Error(`Session ${sessionId} not found`);
|
|
459
575
|
}
|
|
460
|
-
const updated =
|
|
461
|
-
...current,
|
|
462
|
-
...data
|
|
463
|
-
};
|
|
576
|
+
const updated = mergeSessionUpdate(current, data);
|
|
464
577
|
this.sessions.set(sessionKey, updated);
|
|
465
578
|
}
|
|
579
|
+
async patchCredentials(userId, sessionId, data) {
|
|
580
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
581
|
+
if (!this.sessions.has(sessionKey)) {
|
|
582
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
583
|
+
}
|
|
584
|
+
const current = this.credentials.get(sessionKey) ?? { sessionId, userId };
|
|
585
|
+
this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
586
|
+
}
|
|
466
587
|
async get(userId, sessionId) {
|
|
467
588
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
468
589
|
return this.sessions.get(sessionKey) || null;
|
|
469
590
|
}
|
|
591
|
+
async getCredentials(userId, sessionId) {
|
|
592
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
593
|
+
if (!this.sessions.has(sessionKey)) return null;
|
|
594
|
+
return this.credentials.get(sessionKey) ?? { sessionId, userId };
|
|
595
|
+
}
|
|
596
|
+
async clearCredentials(userId, sessionId) {
|
|
597
|
+
await this.patchCredentials(userId, sessionId, {
|
|
598
|
+
clientInformation: null,
|
|
599
|
+
tokens: null,
|
|
600
|
+
codeVerifier: null,
|
|
601
|
+
clientId: null,
|
|
602
|
+
oauthState: null
|
|
603
|
+
});
|
|
604
|
+
}
|
|
470
605
|
async listIds(userId) {
|
|
471
606
|
const set = this.userIdSessions.get(userId);
|
|
472
607
|
return set ? Array.from(set) : [];
|
|
@@ -486,6 +621,7 @@ var MemoryStorageBackend = class {
|
|
|
486
621
|
async delete(userId, sessionId) {
|
|
487
622
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
488
623
|
this.sessions.delete(sessionKey);
|
|
624
|
+
this.credentials.delete(sessionKey);
|
|
489
625
|
const set = this.userIdSessions.get(userId);
|
|
490
626
|
if (set) {
|
|
491
627
|
set.delete(sessionId);
|
|
@@ -499,9 +635,22 @@ var MemoryStorageBackend = class {
|
|
|
499
635
|
}
|
|
500
636
|
async clearAll() {
|
|
501
637
|
this.sessions.clear();
|
|
638
|
+
this.credentials.clear();
|
|
502
639
|
this.userIdSessions.clear();
|
|
503
640
|
}
|
|
504
641
|
async cleanupExpired() {
|
|
642
|
+
for (const [key, session] of this.sessions.entries()) {
|
|
643
|
+
if (!isSessionExpired(session)) continue;
|
|
644
|
+
this.sessions.delete(key);
|
|
645
|
+
this.credentials.delete(key);
|
|
646
|
+
const set = this.userIdSessions.get(session.userId);
|
|
647
|
+
if (set) {
|
|
648
|
+
set.delete(session.sessionId);
|
|
649
|
+
if (set.size === 0) {
|
|
650
|
+
this.userIdSessions.delete(session.userId);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
505
654
|
}
|
|
506
655
|
async disconnect() {
|
|
507
656
|
}
|
|
@@ -516,6 +665,7 @@ var FileStorageBackend = class {
|
|
|
516
665
|
constructor(options = {}) {
|
|
517
666
|
__publicField(this, "filePath");
|
|
518
667
|
__publicField(this, "memoryCache", null);
|
|
668
|
+
__publicField(this, "credentialsCache", null);
|
|
519
669
|
__publicField(this, "initialized", false);
|
|
520
670
|
this.filePath = options.path || "./sessions.json";
|
|
521
671
|
}
|
|
@@ -530,14 +680,22 @@ var FileStorageBackend = class {
|
|
|
530
680
|
const data = await fs2.promises.readFile(this.filePath, "utf-8");
|
|
531
681
|
const json = JSON.parse(data);
|
|
532
682
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
683
|
+
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
684
|
+
if (Array.isArray(json.sessions)) {
|
|
685
|
+
json.sessions.forEach((s) => {
|
|
686
|
+
const session = normalizeStoredSession(s);
|
|
687
|
+
this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
if (Array.isArray(json.credentials)) {
|
|
691
|
+
json.credentials.forEach((c) => {
|
|
692
|
+
this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
|
|
536
693
|
});
|
|
537
694
|
}
|
|
538
695
|
} catch (error) {
|
|
539
696
|
if (error.code === "ENOENT") {
|
|
540
697
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
698
|
+
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
541
699
|
await this.flush();
|
|
542
700
|
} else {
|
|
543
701
|
console.error("[FileStorage] Failed to load sessions:", error);
|
|
@@ -551,9 +709,11 @@ var FileStorageBackend = class {
|
|
|
551
709
|
if (!this.initialized) await this.init();
|
|
552
710
|
}
|
|
553
711
|
async flush() {
|
|
554
|
-
if (!this.memoryCache) return;
|
|
555
|
-
|
|
556
|
-
|
|
712
|
+
if (!this.memoryCache || !this.credentialsCache) return;
|
|
713
|
+
await fs2.promises.writeFile(this.filePath, JSON.stringify({
|
|
714
|
+
sessions: Array.from(this.memoryCache.values()),
|
|
715
|
+
credentials: Array.from(this.credentialsCache.values())
|
|
716
|
+
}, null, 2), "utf-8");
|
|
557
717
|
}
|
|
558
718
|
getSessionKey(userId, sessionId) {
|
|
559
719
|
return `${userId}:${sessionId}`;
|
|
@@ -561,7 +721,7 @@ var FileStorageBackend = class {
|
|
|
561
721
|
generateSessionId() {
|
|
562
722
|
return generateSessionId();
|
|
563
723
|
}
|
|
564
|
-
async create(session
|
|
724
|
+
async create(session) {
|
|
565
725
|
await this.ensureInitialized();
|
|
566
726
|
const { sessionId, userId } = session;
|
|
567
727
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
@@ -569,10 +729,10 @@ var FileStorageBackend = class {
|
|
|
569
729
|
if (this.memoryCache.has(sessionKey)) {
|
|
570
730
|
throw new Error(`Session ${sessionId} already exists`);
|
|
571
731
|
}
|
|
572
|
-
this.memoryCache.set(sessionKey, session);
|
|
732
|
+
this.memoryCache.set(sessionKey, normalizeNewSession(session));
|
|
573
733
|
await this.flush();
|
|
574
734
|
}
|
|
575
|
-
async update(userId, sessionId, data
|
|
735
|
+
async update(userId, sessionId, data) {
|
|
576
736
|
await this.ensureInitialized();
|
|
577
737
|
if (!userId || !sessionId) throw new Error("userId and sessionId required");
|
|
578
738
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
@@ -580,18 +740,40 @@ var FileStorageBackend = class {
|
|
|
580
740
|
if (!current) {
|
|
581
741
|
throw new Error(`Session ${sessionId} not found`);
|
|
582
742
|
}
|
|
583
|
-
const updated =
|
|
584
|
-
...current,
|
|
585
|
-
...data
|
|
586
|
-
};
|
|
743
|
+
const updated = mergeSessionUpdate(current, data);
|
|
587
744
|
this.memoryCache.set(sessionKey, updated);
|
|
588
745
|
await this.flush();
|
|
589
746
|
}
|
|
747
|
+
async patchCredentials(userId, sessionId, data) {
|
|
748
|
+
await this.ensureInitialized();
|
|
749
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
750
|
+
if (!this.memoryCache.has(sessionKey)) {
|
|
751
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
752
|
+
}
|
|
753
|
+
const current = this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
|
|
754
|
+
this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
755
|
+
await this.flush();
|
|
756
|
+
}
|
|
590
757
|
async get(userId, sessionId) {
|
|
591
758
|
await this.ensureInitialized();
|
|
592
759
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
593
760
|
return this.memoryCache.get(sessionKey) || null;
|
|
594
761
|
}
|
|
762
|
+
async getCredentials(userId, sessionId) {
|
|
763
|
+
await this.ensureInitialized();
|
|
764
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
765
|
+
if (!this.memoryCache.has(sessionKey)) return null;
|
|
766
|
+
return this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
|
|
767
|
+
}
|
|
768
|
+
async clearCredentials(userId, sessionId) {
|
|
769
|
+
await this.patchCredentials(userId, sessionId, {
|
|
770
|
+
clientInformation: null,
|
|
771
|
+
tokens: null,
|
|
772
|
+
codeVerifier: null,
|
|
773
|
+
clientId: null,
|
|
774
|
+
oauthState: null
|
|
775
|
+
});
|
|
776
|
+
}
|
|
595
777
|
async list(userId) {
|
|
596
778
|
await this.ensureInitialized();
|
|
597
779
|
return Array.from(this.memoryCache.values()).filter((s) => s.userId === userId);
|
|
@@ -603,7 +785,9 @@ var FileStorageBackend = class {
|
|
|
603
785
|
async delete(userId, sessionId) {
|
|
604
786
|
await this.ensureInitialized();
|
|
605
787
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
606
|
-
|
|
788
|
+
const deleted = this.memoryCache.delete(sessionKey);
|
|
789
|
+
this.credentialsCache.delete(sessionKey);
|
|
790
|
+
if (deleted) {
|
|
607
791
|
await this.flush();
|
|
608
792
|
}
|
|
609
793
|
}
|
|
@@ -614,10 +798,21 @@ var FileStorageBackend = class {
|
|
|
614
798
|
async clearAll() {
|
|
615
799
|
await this.ensureInitialized();
|
|
616
800
|
this.memoryCache.clear();
|
|
801
|
+
this.credentialsCache.clear();
|
|
617
802
|
await this.flush();
|
|
618
803
|
}
|
|
619
804
|
async cleanupExpired() {
|
|
620
805
|
await this.ensureInitialized();
|
|
806
|
+
let changed = false;
|
|
807
|
+
for (const [key, session] of this.memoryCache.entries()) {
|
|
808
|
+
if (!isSessionExpired(session)) continue;
|
|
809
|
+
this.memoryCache.delete(key);
|
|
810
|
+
this.credentialsCache.delete(key);
|
|
811
|
+
changed = true;
|
|
812
|
+
}
|
|
813
|
+
if (changed) {
|
|
814
|
+
await this.flush();
|
|
815
|
+
}
|
|
621
816
|
}
|
|
622
817
|
async disconnect() {
|
|
623
818
|
}
|
|
@@ -629,10 +824,12 @@ var SqliteStorage = class {
|
|
|
629
824
|
constructor(options = {}) {
|
|
630
825
|
__publicField(this, "db", null);
|
|
631
826
|
__publicField(this, "table");
|
|
827
|
+
__publicField(this, "credentialsTable");
|
|
632
828
|
__publicField(this, "initialized", false);
|
|
633
829
|
__publicField(this, "dbPath");
|
|
634
830
|
this.dbPath = options.path || "./sessions.db";
|
|
635
831
|
this.table = options.table || "mcp_sessions";
|
|
832
|
+
this.credentialsTable = `${this.table}_credentials`;
|
|
636
833
|
}
|
|
637
834
|
async init() {
|
|
638
835
|
if (this.initialized) return;
|
|
@@ -643,6 +840,7 @@ var SqliteStorage = class {
|
|
|
643
840
|
fs2__namespace.mkdirSync(dir, { recursive: true });
|
|
644
841
|
}
|
|
645
842
|
this.db = new DatabaseConstructor(this.dbPath);
|
|
843
|
+
this.db.pragma("foreign_keys = ON");
|
|
646
844
|
this.db.exec(`
|
|
647
845
|
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
648
846
|
sessionId TEXT PRIMARY KEY,
|
|
@@ -651,6 +849,13 @@ var SqliteStorage = class {
|
|
|
651
849
|
expiresAt INTEGER
|
|
652
850
|
);
|
|
653
851
|
CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
|
|
852
|
+
CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
|
|
853
|
+
sessionId TEXT NOT NULL,
|
|
854
|
+
userId TEXT NOT NULL,
|
|
855
|
+
data TEXT NOT NULL,
|
|
856
|
+
PRIMARY KEY (userId, sessionId),
|
|
857
|
+
FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
|
|
858
|
+
);
|
|
654
859
|
`);
|
|
655
860
|
this.initialized = true;
|
|
656
861
|
console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
|
|
@@ -671,18 +876,18 @@ var SqliteStorage = class {
|
|
|
671
876
|
generateSessionId() {
|
|
672
877
|
return generateSessionId();
|
|
673
878
|
}
|
|
674
|
-
async create(session
|
|
879
|
+
async create(session) {
|
|
675
880
|
this.ensureInitialized();
|
|
676
881
|
const { sessionId, userId } = session;
|
|
677
882
|
if (!sessionId || !userId) {
|
|
678
883
|
throw new Error("userId and sessionId required");
|
|
679
884
|
}
|
|
680
|
-
const
|
|
885
|
+
const sessionWithLifecycle = normalizeNewSession(session);
|
|
681
886
|
try {
|
|
682
887
|
const stmt = this.db.prepare(
|
|
683
888
|
`INSERT INTO ${this.table} (sessionId, userId, data, expiresAt) VALUES (?, ?, ?, ?)`
|
|
684
889
|
);
|
|
685
|
-
stmt.run(sessionId, userId, JSON.stringify(
|
|
890
|
+
stmt.run(sessionId, userId, JSON.stringify(sessionWithLifecycle), sessionWithLifecycle.expiresAt ?? null);
|
|
686
891
|
} catch (error) {
|
|
687
892
|
if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
688
893
|
throw new Error(`Session ${sessionId} already exists`);
|
|
@@ -690,7 +895,7 @@ var SqliteStorage = class {
|
|
|
690
895
|
throw error;
|
|
691
896
|
}
|
|
692
897
|
}
|
|
693
|
-
async update(userId, sessionId, data
|
|
898
|
+
async update(userId, sessionId, data) {
|
|
694
899
|
this.ensureInitialized();
|
|
695
900
|
if (!sessionId || !userId) {
|
|
696
901
|
throw new Error("userId and sessionId required");
|
|
@@ -699,12 +904,25 @@ var SqliteStorage = class {
|
|
|
699
904
|
if (!currentSession) {
|
|
700
905
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
701
906
|
}
|
|
702
|
-
const updatedSession =
|
|
703
|
-
const expiresAt = ttl ? Date.now() + ttl * 1e3 : null;
|
|
907
|
+
const updatedSession = mergeSessionUpdate(currentSession, data);
|
|
704
908
|
const stmt = this.db.prepare(
|
|
705
909
|
`UPDATE ${this.table} SET data = ?, expiresAt = ? WHERE sessionId = ? AND userId = ?`
|
|
706
910
|
);
|
|
707
|
-
stmt.run(JSON.stringify(updatedSession), expiresAt, sessionId, userId);
|
|
911
|
+
stmt.run(JSON.stringify(updatedSession), updatedSession.expiresAt ?? null, sessionId, userId);
|
|
912
|
+
}
|
|
913
|
+
async patchCredentials(userId, sessionId, data) {
|
|
914
|
+
this.ensureInitialized();
|
|
915
|
+
if (!await this.get(userId, sessionId)) {
|
|
916
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
917
|
+
}
|
|
918
|
+
const current = await this.getCredentials(userId, sessionId) ?? { sessionId, userId };
|
|
919
|
+
const credentials = { ...current, ...data, sessionId, userId };
|
|
920
|
+
const stmt = this.db.prepare(
|
|
921
|
+
`INSERT INTO ${this.credentialsTable} (sessionId, userId, data)
|
|
922
|
+
VALUES (?, ?, ?)
|
|
923
|
+
ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
|
|
924
|
+
);
|
|
925
|
+
stmt.run(sessionId, userId, JSON.stringify(credentials));
|
|
708
926
|
}
|
|
709
927
|
async get(userId, sessionId) {
|
|
710
928
|
this.ensureInitialized();
|
|
@@ -713,7 +931,25 @@ var SqliteStorage = class {
|
|
|
713
931
|
);
|
|
714
932
|
const row = stmt.get(sessionId, userId);
|
|
715
933
|
if (!row) return null;
|
|
716
|
-
return JSON.parse(row.data);
|
|
934
|
+
return normalizeStoredSession(JSON.parse(row.data));
|
|
935
|
+
}
|
|
936
|
+
async getCredentials(userId, sessionId) {
|
|
937
|
+
this.ensureInitialized();
|
|
938
|
+
if (!await this.get(userId, sessionId)) return null;
|
|
939
|
+
const stmt = this.db.prepare(
|
|
940
|
+
`SELECT data FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
941
|
+
);
|
|
942
|
+
const row = stmt.get(sessionId, userId);
|
|
943
|
+
return row ? JSON.parse(row.data) : { sessionId, userId };
|
|
944
|
+
}
|
|
945
|
+
async clearCredentials(userId, sessionId) {
|
|
946
|
+
await this.patchCredentials(userId, sessionId, {
|
|
947
|
+
clientInformation: null,
|
|
948
|
+
tokens: null,
|
|
949
|
+
codeVerifier: null,
|
|
950
|
+
clientId: null,
|
|
951
|
+
oauthState: null
|
|
952
|
+
});
|
|
717
953
|
}
|
|
718
954
|
async list(userId) {
|
|
719
955
|
this.ensureInitialized();
|
|
@@ -721,7 +957,7 @@ var SqliteStorage = class {
|
|
|
721
957
|
`SELECT data FROM ${this.table} WHERE userId = ?`
|
|
722
958
|
);
|
|
723
959
|
const rows = stmt.all(userId);
|
|
724
|
-
return rows.map((row) => JSON.parse(row.data));
|
|
960
|
+
return rows.map((row) => normalizeStoredSession(JSON.parse(row.data)));
|
|
725
961
|
}
|
|
726
962
|
async listIds(userId) {
|
|
727
963
|
this.ensureInitialized();
|
|
@@ -736,6 +972,9 @@ var SqliteStorage = class {
|
|
|
736
972
|
const stmt = this.db.prepare(
|
|
737
973
|
`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
738
974
|
);
|
|
975
|
+
this.db.prepare(
|
|
976
|
+
`DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
977
|
+
).run(sessionId, userId);
|
|
739
978
|
stmt.run(sessionId, userId);
|
|
740
979
|
}
|
|
741
980
|
async listAllIds() {
|
|
@@ -746,16 +985,28 @@ var SqliteStorage = class {
|
|
|
746
985
|
}
|
|
747
986
|
async clearAll() {
|
|
748
987
|
this.ensureInitialized();
|
|
988
|
+
this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
|
|
749
989
|
const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
|
|
750
990
|
stmt.run();
|
|
751
991
|
}
|
|
752
992
|
async cleanupExpired() {
|
|
753
993
|
this.ensureInitialized();
|
|
754
|
-
const
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
994
|
+
const rows = this.db.prepare(`SELECT sessionId, userId, data FROM ${this.table}`).all();
|
|
995
|
+
const deleteStmt = this.db.prepare(`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`);
|
|
996
|
+
for (const row of rows) {
|
|
997
|
+
const session = normalizeStoredSession(JSON.parse(row.data));
|
|
998
|
+
if (isSessionExpired(session)) {
|
|
999
|
+
deleteStmt.run(row.sessionId, row.userId);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
this.db.prepare(
|
|
1003
|
+
`DELETE FROM ${this.credentialsTable}
|
|
1004
|
+
WHERE NOT EXISTS (
|
|
1005
|
+
SELECT 1 FROM ${this.table}
|
|
1006
|
+
WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
|
|
1007
|
+
AND ${this.table}.userId = ${this.credentialsTable}.userId
|
|
1008
|
+
)`
|
|
1009
|
+
).run();
|
|
759
1010
|
}
|
|
760
1011
|
async disconnect() {
|
|
761
1012
|
if (this.db) {
|
|
@@ -840,19 +1091,21 @@ function decryptObject(data) {
|
|
|
840
1091
|
var SupabaseStorageBackend = class {
|
|
841
1092
|
constructor(supabase) {
|
|
842
1093
|
this.supabase = supabase;
|
|
843
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
844
1094
|
}
|
|
845
1095
|
async init() {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1096
|
+
await this.assertTable("mcp_sessions", "session_id");
|
|
1097
|
+
await this.assertTable("mcp_credentials", "session_id");
|
|
1098
|
+
console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
|
|
1099
|
+
}
|
|
1100
|
+
async assertTable(table, column) {
|
|
1101
|
+
const { error } = await this.supabase.from(table).select(column).limit(0);
|
|
1102
|
+
if (!error) return;
|
|
1103
|
+
if (error.code === "42P01") {
|
|
1104
|
+
throw new Error(
|
|
1105
|
+
`[SupabaseStorage] Table "${table}" not found in your database. Please run "npx mcp-ts supabase-init" to set up the required storage schema.`
|
|
1106
|
+
);
|
|
854
1107
|
}
|
|
855
|
-
|
|
1108
|
+
throw new Error(`[SupabaseStorage] Initialization check failed for "${table}": ${error.message}`);
|
|
856
1109
|
}
|
|
857
1110
|
generateSessionId() {
|
|
858
1111
|
return generateSessionId();
|
|
@@ -866,37 +1119,49 @@ var SupabaseStorageBackend = class {
|
|
|
866
1119
|
transportType: row.transport_type,
|
|
867
1120
|
callbackUrl: row.callback_url,
|
|
868
1121
|
createdAt: new Date(row.created_at).getTime(),
|
|
1122
|
+
updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
|
|
1123
|
+
expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
|
|
869
1124
|
userId: row.user_id,
|
|
870
1125
|
headers: decryptObject(row.headers),
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
tokens: decryptObject(row.tokens),
|
|
874
|
-
codeVerifier: row.code_verifier,
|
|
875
|
-
clientId: row.client_id
|
|
1126
|
+
authUrl: row.auth_url,
|
|
1127
|
+
status: row.status ?? "pending"
|
|
876
1128
|
};
|
|
877
1129
|
}
|
|
878
|
-
|
|
1130
|
+
mapRowToCredentials(row, userId, sessionId) {
|
|
1131
|
+
return {
|
|
1132
|
+
sessionId,
|
|
1133
|
+
userId,
|
|
1134
|
+
clientInformation: decryptObject(row?.client_information),
|
|
1135
|
+
tokens: decryptObject(row?.tokens),
|
|
1136
|
+
codeVerifier: decryptObject(row?.code_verifier),
|
|
1137
|
+
clientId: row?.client_id,
|
|
1138
|
+
oauthState: row?.oauth_state
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
hasCredentialData(data) {
|
|
1142
|
+
return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
|
|
1143
|
+
}
|
|
1144
|
+
async create(session) {
|
|
879
1145
|
const { sessionId, userId } = session;
|
|
880
1146
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
881
|
-
const
|
|
882
|
-
const
|
|
1147
|
+
const status = session.status ?? "pending";
|
|
1148
|
+
const createdAt = new Date(session.createdAt || Date.now()).toISOString();
|
|
1149
|
+
const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
|
|
1150
|
+
const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
|
|
883
1151
|
const { error } = await this.supabase.from("mcp_sessions").insert({
|
|
884
1152
|
session_id: sessionId,
|
|
885
1153
|
user_id: userId,
|
|
886
|
-
// Maps user_id to userId to support RLS using auth.uid()
|
|
887
1154
|
server_id: session.serverId,
|
|
888
1155
|
server_name: session.serverName,
|
|
889
1156
|
server_url: session.serverUrl,
|
|
890
1157
|
transport_type: session.transportType,
|
|
891
1158
|
callback_url: session.callbackUrl,
|
|
892
|
-
created_at:
|
|
1159
|
+
created_at: createdAt,
|
|
1160
|
+
updated_at: updatedAt,
|
|
893
1161
|
headers: encryptObject(session.headers),
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
code_verifier: session.codeVerifier,
|
|
898
|
-
client_id: session.clientId,
|
|
899
|
-
expires_at: expiresAt
|
|
1162
|
+
auth_url: session.authUrl ?? null,
|
|
1163
|
+
status,
|
|
1164
|
+
expires_at: expiresAt === null ? null : new Date(expiresAt).toISOString()
|
|
900
1165
|
});
|
|
901
1166
|
if (error) {
|
|
902
1167
|
if (error.code === "23505") {
|
|
@@ -905,11 +1170,8 @@ var SupabaseStorageBackend = class {
|
|
|
905
1170
|
throw new Error(`Failed to create session in Supabase: ${error.message}`);
|
|
906
1171
|
}
|
|
907
1172
|
}
|
|
908
|
-
async update(userId, sessionId, data
|
|
909
|
-
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
910
|
-
const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
|
|
1173
|
+
async update(userId, sessionId, data) {
|
|
911
1174
|
const updateData = {
|
|
912
|
-
expires_at: expiresAt,
|
|
913
1175
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
914
1176
|
};
|
|
915
1177
|
if ("serverId" in data) updateData.server_id = data.serverId;
|
|
@@ -917,20 +1179,50 @@ var SupabaseStorageBackend = class {
|
|
|
917
1179
|
if ("serverUrl" in data) updateData.server_url = data.serverUrl;
|
|
918
1180
|
if ("transportType" in data) updateData.transport_type = data.transportType;
|
|
919
1181
|
if ("callbackUrl" in data) updateData.callback_url = data.callbackUrl;
|
|
920
|
-
if ("
|
|
1182
|
+
if ("status" in data) {
|
|
1183
|
+
const status = data.status ?? "pending";
|
|
1184
|
+
const expiresAt = resolveSessionExpiresAt(status);
|
|
1185
|
+
updateData.status = status;
|
|
1186
|
+
updateData.expires_at = expiresAt === null ? null : new Date(expiresAt).toISOString();
|
|
1187
|
+
}
|
|
921
1188
|
if ("headers" in data) updateData.headers = encryptObject(data.headers);
|
|
922
|
-
if ("
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
if (
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1189
|
+
if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
|
|
1190
|
+
const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
|
|
1191
|
+
let updatedRows = null;
|
|
1192
|
+
if (shouldUpdateSession) {
|
|
1193
|
+
const result = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId).select("id");
|
|
1194
|
+
if (result.error) {
|
|
1195
|
+
throw new Error(`Failed to update session: ${result.error.message}`);
|
|
1196
|
+
}
|
|
1197
|
+
updatedRows = result.data;
|
|
1198
|
+
} else {
|
|
1199
|
+
const result = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
|
|
1200
|
+
if (result.error) {
|
|
1201
|
+
throw new Error(`Failed to update session: ${result.error.message}`);
|
|
1202
|
+
}
|
|
1203
|
+
updatedRows = result.data;
|
|
929
1204
|
}
|
|
930
1205
|
if (!updatedRows || updatedRows.length === 0) {
|
|
931
1206
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
932
1207
|
}
|
|
933
1208
|
}
|
|
1209
|
+
async patchCredentials(userId, sessionId, data) {
|
|
1210
|
+
if (!this.hasCredentialData(data)) return;
|
|
1211
|
+
const row = {
|
|
1212
|
+
user_id: userId,
|
|
1213
|
+
session_id: sessionId,
|
|
1214
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1215
|
+
};
|
|
1216
|
+
if ("clientInformation" in data) row.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
|
|
1217
|
+
if ("tokens" in data) row.tokens = data.tokens == null ? null : encryptObject(data.tokens);
|
|
1218
|
+
if ("codeVerifier" in data) row.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
|
|
1219
|
+
if ("clientId" in data) row.client_id = data.clientId ?? null;
|
|
1220
|
+
if ("oauthState" in data) row.oauth_state = data.oauthState ?? null;
|
|
1221
|
+
const { error } = await this.supabase.from("mcp_credentials").upsert(row, { onConflict: "user_id,session_id" });
|
|
1222
|
+
if (error) {
|
|
1223
|
+
throw new Error(`Failed to update credentials: ${error.message}`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
934
1226
|
async get(userId, sessionId) {
|
|
935
1227
|
const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
936
1228
|
if (error) {
|
|
@@ -940,6 +1232,21 @@ var SupabaseStorageBackend = class {
|
|
|
940
1232
|
if (!data) return null;
|
|
941
1233
|
return this.mapRowToSessionData(data);
|
|
942
1234
|
}
|
|
1235
|
+
async getCredentials(userId, sessionId) {
|
|
1236
|
+
const { data, error } = await this.supabase.from("mcp_credentials").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
1237
|
+
if (error) {
|
|
1238
|
+
console.error("[SupabaseStorage] Failed to get credentials:", error);
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
if (data) {
|
|
1242
|
+
return this.mapRowToCredentials(data, userId, sessionId);
|
|
1243
|
+
}
|
|
1244
|
+
const { data: sessionRows, error: sessionError } = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
|
|
1245
|
+
if (sessionError || !sessionRows || sessionRows.length === 0) {
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
return { sessionId, userId };
|
|
1249
|
+
}
|
|
943
1250
|
async list(userId) {
|
|
944
1251
|
const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
|
|
945
1252
|
if (error) {
|
|
@@ -948,6 +1255,12 @@ var SupabaseStorageBackend = class {
|
|
|
948
1255
|
}
|
|
949
1256
|
return data.map((row) => this.mapRowToSessionData(row));
|
|
950
1257
|
}
|
|
1258
|
+
async clearCredentials(userId, sessionId) {
|
|
1259
|
+
const { error } = await this.supabase.from("mcp_credentials").delete().eq("user_id", userId).eq("session_id", sessionId);
|
|
1260
|
+
if (error) {
|
|
1261
|
+
throw new Error(`Failed to clear credentials: ${error.message}`);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
951
1264
|
async delete(userId, sessionId) {
|
|
952
1265
|
const { error } = await this.supabase.from("mcp_sessions").delete().eq("user_id", userId).eq("session_id", sessionId);
|
|
953
1266
|
if (error) {
|
|
@@ -971,15 +1284,24 @@ var SupabaseStorageBackend = class {
|
|
|
971
1284
|
return data.map((row) => row.session_id);
|
|
972
1285
|
}
|
|
973
1286
|
async clearAll() {
|
|
1287
|
+
const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
|
|
1288
|
+
if (credentialsError) {
|
|
1289
|
+
console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
|
|
1290
|
+
}
|
|
974
1291
|
const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
|
|
975
1292
|
if (error) {
|
|
976
1293
|
console.error("[SupabaseStorage] Failed to clear sessions:", error);
|
|
977
1294
|
}
|
|
978
1295
|
}
|
|
979
1296
|
async cleanupExpired() {
|
|
980
|
-
const { error } = await this.supabase.from("mcp_sessions").delete().lt("expires_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
981
|
-
if (
|
|
982
|
-
console.error("[SupabaseStorage] Failed to cleanup expired sessions:",
|
|
1297
|
+
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());
|
|
1298
|
+
if (transientError) {
|
|
1299
|
+
console.error("[SupabaseStorage] Failed to cleanup expired inactive sessions:", transientError);
|
|
1300
|
+
}
|
|
1301
|
+
const dormantCutoff = new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString();
|
|
1302
|
+
const { error: dormantError } = await this.supabase.from("mcp_sessions").delete().eq("status", "active").lt("updated_at", dormantCutoff);
|
|
1303
|
+
if (dormantError) {
|
|
1304
|
+
console.error("[SupabaseStorage] Failed to cleanup dormant active sessions:", dormantError);
|
|
983
1305
|
}
|
|
984
1306
|
}
|
|
985
1307
|
async disconnect() {
|
|
@@ -991,23 +1313,29 @@ init_cjs_shims();
|
|
|
991
1313
|
var NeonStorageBackend = class {
|
|
992
1314
|
constructor(sql, options = {}) {
|
|
993
1315
|
this.sql = sql;
|
|
994
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
995
1316
|
__publicField(this, "tableName");
|
|
1317
|
+
__publicField(this, "credentialsTableName");
|
|
996
1318
|
const schema = options.schema || "public";
|
|
997
1319
|
const table = options.table || "mcp_sessions";
|
|
1320
|
+
const credentialsTable = options.credentialsTable || "mcp_credentials";
|
|
998
1321
|
this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
|
|
1322
|
+
this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
|
|
999
1323
|
}
|
|
1000
1324
|
async init() {
|
|
1325
|
+
await this.assertTable(this.tableName, "mcp_sessions");
|
|
1326
|
+
await this.assertTable(this.credentialsTableName, "mcp_credentials");
|
|
1327
|
+
console.log("[mcp-ts][Storage] Neon: storage tables verified.");
|
|
1328
|
+
}
|
|
1329
|
+
async assertTable(qualifiedName, displayName) {
|
|
1001
1330
|
const [{ exists } = { exists: null }] = await this.sql.query(
|
|
1002
1331
|
"SELECT to_regclass($1) AS exists",
|
|
1003
|
-
[
|
|
1332
|
+
[qualifiedName.replace(/"/g, "")]
|
|
1004
1333
|
);
|
|
1005
1334
|
if (!exists) {
|
|
1006
1335
|
throw new Error(
|
|
1007
|
-
|
|
1336
|
+
`[NeonStorage] Table "${displayName}" not found in your database. Please create it using the Neon storage guide in docs/storage-backends/neon.md.`
|
|
1008
1337
|
);
|
|
1009
1338
|
}
|
|
1010
|
-
console.log('[mcp-ts][Storage] Neon: "mcp_sessions" table verified.');
|
|
1011
1339
|
}
|
|
1012
1340
|
generateSessionId() {
|
|
1013
1341
|
return generateSessionId();
|
|
@@ -1027,20 +1355,35 @@ var NeonStorageBackend = class {
|
|
|
1027
1355
|
transportType: row.transport_type,
|
|
1028
1356
|
callbackUrl: row.callback_url,
|
|
1029
1357
|
createdAt: new Date(row.created_at).getTime(),
|
|
1358
|
+
updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
|
|
1359
|
+
expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
|
|
1030
1360
|
userId: row.user_id,
|
|
1031
1361
|
headers: decryptObject(row.headers),
|
|
1032
|
-
|
|
1033
|
-
|
|
1362
|
+
authUrl: row.auth_url ?? void 0,
|
|
1363
|
+
status: row.status ?? "pending"
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
mapRowToCredentials(row, userId, sessionId) {
|
|
1367
|
+
return {
|
|
1368
|
+
sessionId,
|
|
1369
|
+
userId,
|
|
1370
|
+
clientInformation: decryptObject(row.client_information),
|
|
1034
1371
|
tokens: decryptObject(row.tokens),
|
|
1035
|
-
codeVerifier: row.code_verifier
|
|
1036
|
-
clientId: row.client_id ?? void 0
|
|
1372
|
+
codeVerifier: decryptObject(row.code_verifier),
|
|
1373
|
+
clientId: row.client_id ?? void 0,
|
|
1374
|
+
oauthState: row.oauth_state
|
|
1037
1375
|
};
|
|
1038
1376
|
}
|
|
1039
|
-
|
|
1377
|
+
hasCredentialData(data) {
|
|
1378
|
+
return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
|
|
1379
|
+
}
|
|
1380
|
+
async create(session) {
|
|
1040
1381
|
const { sessionId, userId } = session;
|
|
1041
1382
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
1042
|
-
const
|
|
1043
|
-
const
|
|
1383
|
+
const status = session.status ?? "pending";
|
|
1384
|
+
const createdAt = new Date(session.createdAt || Date.now()).toISOString();
|
|
1385
|
+
const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
|
|
1386
|
+
const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
|
|
1044
1387
|
try {
|
|
1045
1388
|
await this.sql.query(
|
|
1046
1389
|
`INSERT INTO ${this.tableName} (
|
|
@@ -1052,16 +1395,14 @@ var NeonStorageBackend = class {
|
|
|
1052
1395
|
transport_type,
|
|
1053
1396
|
callback_url,
|
|
1054
1397
|
created_at,
|
|
1398
|
+
updated_at,
|
|
1055
1399
|
headers,
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
tokens,
|
|
1059
|
-
code_verifier,
|
|
1060
|
-
client_id,
|
|
1400
|
+
auth_url,
|
|
1401
|
+
status,
|
|
1061
1402
|
expires_at
|
|
1062
1403
|
) VALUES (
|
|
1063
1404
|
$1, $2, $3, $4, $5, $6, $7, $8,
|
|
1064
|
-
$9, $10, $11, $12, $13
|
|
1405
|
+
$9, $10, $11, $12, $13
|
|
1065
1406
|
)`,
|
|
1066
1407
|
[
|
|
1067
1408
|
sessionId,
|
|
@@ -1071,14 +1412,12 @@ var NeonStorageBackend = class {
|
|
|
1071
1412
|
session.serverUrl,
|
|
1072
1413
|
session.transportType,
|
|
1073
1414
|
session.callbackUrl,
|
|
1074
|
-
|
|
1415
|
+
createdAt,
|
|
1416
|
+
updatedAt,
|
|
1075
1417
|
encryptObject(session.headers),
|
|
1076
|
-
session.
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
session.codeVerifier,
|
|
1080
|
-
session.clientId,
|
|
1081
|
-
expiresAt
|
|
1418
|
+
session.authUrl ?? null,
|
|
1419
|
+
status,
|
|
1420
|
+
expiresAt === null ? null : new Date(expiresAt).toISOString()
|
|
1082
1421
|
]
|
|
1083
1422
|
);
|
|
1084
1423
|
} catch (error) {
|
|
@@ -1088,52 +1427,86 @@ var NeonStorageBackend = class {
|
|
|
1088
1427
|
throw new Error(`Failed to create session in Neon: ${error.message}`);
|
|
1089
1428
|
}
|
|
1090
1429
|
}
|
|
1091
|
-
async update(userId, sessionId, data
|
|
1430
|
+
async update(userId, sessionId, data) {
|
|
1092
1431
|
const currentSession = await this.get(userId, sessionId);
|
|
1093
1432
|
if (!currentSession) {
|
|
1094
1433
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1095
1434
|
}
|
|
1096
1435
|
const updatedSession = { ...currentSession, ...data };
|
|
1097
|
-
const
|
|
1098
|
-
const expiresAt =
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1436
|
+
const status = updatedSession.status ?? "pending";
|
|
1437
|
+
const expiresAt = resolveSessionExpiresAt(status);
|
|
1438
|
+
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;
|
|
1439
|
+
if (shouldUpdateSession) {
|
|
1440
|
+
const updatedRows = await this.sql.query(
|
|
1441
|
+
`UPDATE ${this.tableName}
|
|
1442
|
+
SET
|
|
1443
|
+
server_id = $1,
|
|
1444
|
+
server_name = $2,
|
|
1445
|
+
server_url = $3,
|
|
1446
|
+
transport_type = $4,
|
|
1447
|
+
callback_url = $5,
|
|
1448
|
+
status = $6,
|
|
1449
|
+
headers = $7,
|
|
1450
|
+
auth_url = $8,
|
|
1451
|
+
expires_at = $9,
|
|
1452
|
+
updated_at = now()
|
|
1453
|
+
WHERE user_id = $10 AND session_id = $11
|
|
1454
|
+
RETURNING id`,
|
|
1455
|
+
[
|
|
1456
|
+
updatedSession.serverId,
|
|
1457
|
+
updatedSession.serverName,
|
|
1458
|
+
updatedSession.serverUrl,
|
|
1459
|
+
updatedSession.transportType,
|
|
1460
|
+
updatedSession.callbackUrl,
|
|
1461
|
+
status,
|
|
1462
|
+
encryptObject(updatedSession.headers),
|
|
1463
|
+
updatedSession.authUrl ?? null,
|
|
1464
|
+
expiresAt === null ? null : new Date(expiresAt).toISOString(),
|
|
1465
|
+
userId,
|
|
1466
|
+
sessionId
|
|
1467
|
+
]
|
|
1468
|
+
);
|
|
1469
|
+
if (updatedRows.length === 0) {
|
|
1470
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
async patchCredentials(userId, sessionId, data) {
|
|
1475
|
+
if (!this.hasCredentialData(data)) return;
|
|
1476
|
+
await this.sql.query(
|
|
1477
|
+
`INSERT INTO ${this.credentialsTableName} (
|
|
1478
|
+
user_id,
|
|
1479
|
+
session_id,
|
|
1480
|
+
client_information,
|
|
1481
|
+
tokens,
|
|
1482
|
+
code_verifier,
|
|
1483
|
+
client_id,
|
|
1484
|
+
oauth_state,
|
|
1485
|
+
updated_at
|
|
1486
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, now())
|
|
1487
|
+
ON CONFLICT (user_id, session_id)
|
|
1488
|
+
DO UPDATE SET
|
|
1489
|
+
client_information = CASE WHEN $8 THEN EXCLUDED.client_information ELSE ${this.credentialsTableName}.client_information END,
|
|
1490
|
+
tokens = CASE WHEN $9 THEN EXCLUDED.tokens ELSE ${this.credentialsTableName}.tokens END,
|
|
1491
|
+
code_verifier = CASE WHEN $10 THEN EXCLUDED.code_verifier ELSE ${this.credentialsTableName}.code_verifier END,
|
|
1492
|
+
client_id = CASE WHEN $11 THEN EXCLUDED.client_id ELSE ${this.credentialsTableName}.client_id END,
|
|
1493
|
+
oauth_state = CASE WHEN $12 THEN EXCLUDED.oauth_state ELSE ${this.credentialsTableName}.oauth_state END,
|
|
1494
|
+
updated_at = now()`,
|
|
1117
1495
|
[
|
|
1118
|
-
updatedSession.serverId,
|
|
1119
|
-
updatedSession.serverName,
|
|
1120
|
-
updatedSession.serverUrl,
|
|
1121
|
-
updatedSession.transportType,
|
|
1122
|
-
updatedSession.callbackUrl,
|
|
1123
|
-
updatedSession.active ?? false,
|
|
1124
|
-
encryptObject(updatedSession.headers),
|
|
1125
|
-
updatedSession.clientInformation,
|
|
1126
|
-
updatedSession.tokens === void 0 ? null : encryptObject(updatedSession.tokens),
|
|
1127
|
-
updatedSession.codeVerifier,
|
|
1128
|
-
updatedSession.clientId,
|
|
1129
|
-
expiresAt,
|
|
1130
1496
|
userId,
|
|
1131
|
-
sessionId
|
|
1497
|
+
sessionId,
|
|
1498
|
+
"clientInformation" in data ? data.clientInformation == null ? null : encryptObject(data.clientInformation) : null,
|
|
1499
|
+
"tokens" in data ? data.tokens == null ? null : encryptObject(data.tokens) : null,
|
|
1500
|
+
"codeVerifier" in data ? data.codeVerifier == null ? null : encryptObject(data.codeVerifier) : null,
|
|
1501
|
+
"clientId" in data ? data.clientId ?? null : null,
|
|
1502
|
+
"oauthState" in data ? data.oauthState ?? null : null,
|
|
1503
|
+
"clientInformation" in data,
|
|
1504
|
+
"tokens" in data,
|
|
1505
|
+
"codeVerifier" in data,
|
|
1506
|
+
"clientId" in data,
|
|
1507
|
+
"oauthState" in data
|
|
1132
1508
|
]
|
|
1133
1509
|
);
|
|
1134
|
-
if (updatedRows.length === 0) {
|
|
1135
|
-
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1136
|
-
}
|
|
1137
1510
|
}
|
|
1138
1511
|
async get(userId, sessionId) {
|
|
1139
1512
|
try {
|
|
@@ -1141,12 +1514,32 @@ var NeonStorageBackend = class {
|
|
|
1141
1514
|
`SELECT * FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1142
1515
|
[userId, sessionId]
|
|
1143
1516
|
);
|
|
1144
|
-
|
|
1517
|
+
if (!rows[0]) return null;
|
|
1518
|
+
return this.mapRowToSessionData(rows[0]);
|
|
1145
1519
|
} catch (error) {
|
|
1146
1520
|
console.error("[NeonStorage] Failed to get session:", error);
|
|
1147
1521
|
return null;
|
|
1148
1522
|
}
|
|
1149
1523
|
}
|
|
1524
|
+
async getCredentials(userId, sessionId) {
|
|
1525
|
+
try {
|
|
1526
|
+
const credentialRows = await this.sql.query(
|
|
1527
|
+
`SELECT * FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1528
|
+
[userId, sessionId]
|
|
1529
|
+
);
|
|
1530
|
+
if (credentialRows[0]) {
|
|
1531
|
+
return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
|
|
1532
|
+
}
|
|
1533
|
+
const sessionRows = await this.sql.query(
|
|
1534
|
+
`SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1535
|
+
[userId, sessionId]
|
|
1536
|
+
);
|
|
1537
|
+
return sessionRows[0] ? { sessionId, userId } : null;
|
|
1538
|
+
} catch (error) {
|
|
1539
|
+
console.error("[NeonStorage] Failed to get credentials:", error);
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1150
1543
|
async list(userId) {
|
|
1151
1544
|
try {
|
|
1152
1545
|
const rows = await this.sql.query(
|
|
@@ -1159,6 +1552,16 @@ var NeonStorageBackend = class {
|
|
|
1159
1552
|
return [];
|
|
1160
1553
|
}
|
|
1161
1554
|
}
|
|
1555
|
+
async clearCredentials(userId, sessionId) {
|
|
1556
|
+
try {
|
|
1557
|
+
await this.sql.query(
|
|
1558
|
+
`DELETE FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1559
|
+
[userId, sessionId]
|
|
1560
|
+
);
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
console.error("[NeonStorage] Failed to clear credentials:", error);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1162
1565
|
async delete(userId, sessionId) {
|
|
1163
1566
|
try {
|
|
1164
1567
|
await this.sql.query(
|
|
@@ -1194,6 +1597,7 @@ var NeonStorageBackend = class {
|
|
|
1194
1597
|
}
|
|
1195
1598
|
async clearAll() {
|
|
1196
1599
|
try {
|
|
1600
|
+
await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
|
|
1197
1601
|
await this.sql.query(`DELETE FROM ${this.tableName}`);
|
|
1198
1602
|
} catch (error) {
|
|
1199
1603
|
console.error("[NeonStorage] Failed to clear sessions:", error);
|
|
@@ -1202,9 +1606,17 @@ var NeonStorageBackend = class {
|
|
|
1202
1606
|
async cleanupExpired() {
|
|
1203
1607
|
try {
|
|
1204
1608
|
await this.sql.query(
|
|
1205
|
-
`DELETE FROM ${this.tableName}
|
|
1609
|
+
`DELETE FROM ${this.tableName}
|
|
1610
|
+
WHERE expires_at IS NOT NULL
|
|
1611
|
+
AND expires_at < $1
|
|
1612
|
+
AND status <> 'active'`,
|
|
1206
1613
|
[(/* @__PURE__ */ new Date()).toISOString()]
|
|
1207
1614
|
);
|
|
1615
|
+
await this.sql.query(
|
|
1616
|
+
`DELETE FROM ${this.tableName}
|
|
1617
|
+
WHERE status = 'active' AND updated_at < $1`,
|
|
1618
|
+
[new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString()]
|
|
1619
|
+
);
|
|
1208
1620
|
} catch (error) {
|
|
1209
1621
|
console.error("[NeonStorage] Failed to cleanup expired sessions:", error);
|
|
1210
1622
|
}
|
|
@@ -1254,26 +1666,24 @@ function emitSessionMutation(event) {
|
|
|
1254
1666
|
function createSessionMutationEvent(prop, args) {
|
|
1255
1667
|
const timestamp = Date.now();
|
|
1256
1668
|
if (prop === "create") {
|
|
1257
|
-
const [session
|
|
1669
|
+
const [session] = args;
|
|
1258
1670
|
if (!session?.userId || !session?.sessionId) return null;
|
|
1259
1671
|
return {
|
|
1260
1672
|
type: "create",
|
|
1261
1673
|
userId: session.userId,
|
|
1262
1674
|
sessionId: session.sessionId,
|
|
1263
1675
|
session,
|
|
1264
|
-
ttl,
|
|
1265
1676
|
timestamp
|
|
1266
1677
|
};
|
|
1267
1678
|
}
|
|
1268
1679
|
if (prop === "update") {
|
|
1269
|
-
const [userId, sessionId, patch
|
|
1680
|
+
const [userId, sessionId, patch] = args;
|
|
1270
1681
|
if (!userId || !sessionId) return null;
|
|
1271
1682
|
return {
|
|
1272
1683
|
type: "update",
|
|
1273
1684
|
userId,
|
|
1274
1685
|
sessionId,
|
|
1275
1686
|
patch,
|
|
1276
|
-
ttl,
|
|
1277
1687
|
timestamp
|
|
1278
1688
|
};
|
|
1279
1689
|
}
|
|
@@ -1469,7 +1879,6 @@ var StorageOAuthClientProvider = class {
|
|
|
1469
1879
|
__publicField(this, "_authUrl");
|
|
1470
1880
|
__publicField(this, "_clientId");
|
|
1471
1881
|
__publicField(this, "onRedirectCallback");
|
|
1472
|
-
__publicField(this, "tokenExpiresAt");
|
|
1473
1882
|
this.userId = options.userId;
|
|
1474
1883
|
this.serverId = options.serverId;
|
|
1475
1884
|
this.sessionId = options.sessionId;
|
|
@@ -1503,30 +1912,30 @@ var StorageOAuthClientProvider = class {
|
|
|
1503
1912
|
this._clientId = clientId_;
|
|
1504
1913
|
}
|
|
1505
1914
|
/**
|
|
1506
|
-
* Loads OAuth
|
|
1915
|
+
* Loads OAuth credentials from the session store
|
|
1507
1916
|
* @private
|
|
1508
1917
|
*/
|
|
1509
|
-
async
|
|
1510
|
-
const data = await sessions.
|
|
1918
|
+
async getCredentials() {
|
|
1919
|
+
const data = await sessions.getCredentials(this.userId, this.sessionId);
|
|
1511
1920
|
if (!data) {
|
|
1512
|
-
return {};
|
|
1921
|
+
return { userId: this.userId, sessionId: this.sessionId };
|
|
1513
1922
|
}
|
|
1514
1923
|
return data;
|
|
1515
1924
|
}
|
|
1516
1925
|
/**
|
|
1517
|
-
* Saves OAuth
|
|
1518
|
-
* @param data - Partial OAuth
|
|
1926
|
+
* Saves OAuth credentials to the session store
|
|
1927
|
+
* @param data - Partial OAuth credentials to save
|
|
1519
1928
|
* @private
|
|
1520
1929
|
* @throws Error if session doesn't exist (session must be created by controller layer)
|
|
1521
1930
|
*/
|
|
1522
|
-
async
|
|
1523
|
-
await sessions.
|
|
1931
|
+
async patchCredentials(data) {
|
|
1932
|
+
await sessions.patchCredentials(this.userId, this.sessionId, data);
|
|
1524
1933
|
}
|
|
1525
1934
|
/**
|
|
1526
1935
|
* Retrieves stored OAuth client information
|
|
1527
1936
|
*/
|
|
1528
1937
|
async clientInformation() {
|
|
1529
|
-
const data = await this.
|
|
1938
|
+
const data = await this.getCredentials();
|
|
1530
1939
|
if (data.clientId && !this._clientId) {
|
|
1531
1940
|
this._clientId = data.clientId;
|
|
1532
1941
|
}
|
|
@@ -1545,7 +1954,7 @@ var StorageOAuthClientProvider = class {
|
|
|
1545
1954
|
* Stores OAuth client information
|
|
1546
1955
|
*/
|
|
1547
1956
|
async saveClientInformation(clientInformation) {
|
|
1548
|
-
await this.
|
|
1957
|
+
await this.patchCredentials({
|
|
1549
1958
|
clientInformation,
|
|
1550
1959
|
clientId: clientInformation.client_id
|
|
1551
1960
|
});
|
|
@@ -1555,29 +1964,58 @@ var StorageOAuthClientProvider = class {
|
|
|
1555
1964
|
* Stores OAuth tokens
|
|
1556
1965
|
*/
|
|
1557
1966
|
async saveTokens(tokens) {
|
|
1558
|
-
|
|
1559
|
-
if (tokens.expires_in) {
|
|
1560
|
-
this.tokenExpiresAt = Date.now() + tokens.expires_in * 1e3 - TOKEN_EXPIRY_BUFFER_MS;
|
|
1561
|
-
}
|
|
1562
|
-
await this.saveSessionData(data);
|
|
1967
|
+
await this.patchCredentials({ tokens });
|
|
1563
1968
|
}
|
|
1564
1969
|
get authUrl() {
|
|
1565
1970
|
return this._authUrl;
|
|
1566
1971
|
}
|
|
1567
1972
|
async state() {
|
|
1568
|
-
|
|
1973
|
+
const nonce = nanoid.nanoid(32);
|
|
1974
|
+
await this.patchCredentials({
|
|
1975
|
+
oauthState: {
|
|
1976
|
+
nonce,
|
|
1977
|
+
sessionId: this.sessionId,
|
|
1978
|
+
serverId: this.serverId,
|
|
1979
|
+
createdAt: Date.now()
|
|
1980
|
+
},
|
|
1981
|
+
codeVerifier: null
|
|
1982
|
+
});
|
|
1983
|
+
return formatOAuthState(nonce, this.sessionId);
|
|
1569
1984
|
}
|
|
1570
|
-
async checkState(
|
|
1571
|
-
const
|
|
1985
|
+
async checkState(state) {
|
|
1986
|
+
const parsed = parseOAuthState(state);
|
|
1987
|
+
if (!parsed) {
|
|
1988
|
+
return { valid: false, error: "Invalid OAuth state" };
|
|
1989
|
+
}
|
|
1990
|
+
if (parsed.sessionId !== this.sessionId) {
|
|
1991
|
+
return { valid: false, error: "OAuth state mismatch" };
|
|
1992
|
+
}
|
|
1993
|
+
const data = await sessions.getCredentials(this.userId, parsed.sessionId);
|
|
1572
1994
|
if (!data) {
|
|
1573
1995
|
return { valid: false, error: "Session not found" };
|
|
1574
1996
|
}
|
|
1575
|
-
|
|
1997
|
+
const oauthState = data.oauthState;
|
|
1998
|
+
if (!oauthState) {
|
|
1999
|
+
return { valid: false, error: "OAuth state not found" };
|
|
2000
|
+
}
|
|
2001
|
+
if (oauthState.nonce !== parsed.nonce || oauthState.sessionId !== parsed.sessionId || oauthState.serverId !== this.serverId) {
|
|
2002
|
+
return { valid: false, error: "OAuth state mismatch" };
|
|
2003
|
+
}
|
|
2004
|
+
if (Date.now() - oauthState.createdAt > STATE_EXPIRATION_MS) {
|
|
2005
|
+
return { valid: false, error: "OAuth state expired" };
|
|
2006
|
+
}
|
|
2007
|
+
return { valid: true, serverId: oauthState.serverId };
|
|
1576
2008
|
}
|
|
1577
|
-
async consumeState(
|
|
2009
|
+
async consumeState(state) {
|
|
2010
|
+
const result = await this.checkState(state);
|
|
2011
|
+
if (!result.valid) {
|
|
2012
|
+
throw new Error(result.error || "Invalid OAuth state");
|
|
2013
|
+
}
|
|
2014
|
+
await this.patchCredentials({ oauthState: null });
|
|
1578
2015
|
}
|
|
1579
2016
|
async redirectToAuthorization(authUrl) {
|
|
1580
2017
|
this._authUrl = authUrl.toString();
|
|
2018
|
+
await sessions.update(this.userId, this.sessionId, { authUrl: this._authUrl });
|
|
1581
2019
|
if (this.onRedirectCallback) {
|
|
1582
2020
|
this.onRedirectCallback(authUrl.toString());
|
|
1583
2021
|
}
|
|
@@ -1588,21 +2026,25 @@ var StorageOAuthClientProvider = class {
|
|
|
1588
2026
|
} else {
|
|
1589
2027
|
const updates = {};
|
|
1590
2028
|
if (scope === "client") {
|
|
1591
|
-
updates.clientInformation =
|
|
1592
|
-
updates.clientId =
|
|
2029
|
+
updates.clientInformation = null;
|
|
2030
|
+
updates.clientId = null;
|
|
1593
2031
|
} else if (scope === "tokens") {
|
|
1594
|
-
updates.tokens =
|
|
2032
|
+
updates.tokens = null;
|
|
1595
2033
|
} else if (scope === "verifier") {
|
|
1596
|
-
updates.codeVerifier =
|
|
2034
|
+
updates.codeVerifier = null;
|
|
1597
2035
|
}
|
|
1598
|
-
await this.
|
|
2036
|
+
await this.patchCredentials(updates);
|
|
1599
2037
|
}
|
|
1600
2038
|
}
|
|
1601
2039
|
async saveCodeVerifier(verifier) {
|
|
1602
|
-
await this.
|
|
2040
|
+
const data = await this.getCredentials();
|
|
2041
|
+
if (data.codeVerifier) {
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
await this.patchCredentials({ codeVerifier: verifier });
|
|
1603
2045
|
}
|
|
1604
2046
|
async codeVerifier() {
|
|
1605
|
-
const data = await this.
|
|
2047
|
+
const data = await this.getCredentials();
|
|
1606
2048
|
if (data.clientId && !this._clientId) {
|
|
1607
2049
|
this._clientId = data.clientId;
|
|
1608
2050
|
}
|
|
@@ -1612,23 +2054,14 @@ var StorageOAuthClientProvider = class {
|
|
|
1612
2054
|
return data.codeVerifier;
|
|
1613
2055
|
}
|
|
1614
2056
|
async deleteCodeVerifier() {
|
|
1615
|
-
await this.
|
|
2057
|
+
await this.patchCredentials({ codeVerifier: null });
|
|
1616
2058
|
}
|
|
1617
2059
|
async tokens() {
|
|
1618
|
-
const data = await this.
|
|
2060
|
+
const data = await this.getCredentials();
|
|
1619
2061
|
if (data.clientId && !this._clientId) {
|
|
1620
2062
|
this._clientId = data.clientId;
|
|
1621
2063
|
}
|
|
1622
|
-
return data.tokens;
|
|
1623
|
-
}
|
|
1624
|
-
isTokenExpired() {
|
|
1625
|
-
if (!this.tokenExpiresAt) {
|
|
1626
|
-
return false;
|
|
1627
|
-
}
|
|
1628
|
-
return Date.now() >= this.tokenExpiresAt;
|
|
1629
|
-
}
|
|
1630
|
-
setTokenExpiresAt(expiresAt) {
|
|
1631
|
-
this.tokenExpiresAt = expiresAt;
|
|
2064
|
+
return data.tokens ?? void 0;
|
|
1632
2065
|
}
|
|
1633
2066
|
};
|
|
1634
2067
|
|
|
@@ -1855,7 +2288,7 @@ var MCPClient = class {
|
|
|
1855
2288
|
* Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
|
|
1856
2289
|
* This wrapper enforces a timeout and properly uses AbortController to unblock the request.
|
|
1857
2290
|
*/
|
|
1858
|
-
fetch: (url, init) => {
|
|
2291
|
+
fetch: async (url, init) => {
|
|
1859
2292
|
const timeout = 3e4;
|
|
1860
2293
|
const controller = new AbortController();
|
|
1861
2294
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
@@ -1863,7 +2296,17 @@ var MCPClient = class {
|
|
|
1863
2296
|
// @ts-ignore: AbortSignal.any is available in Node 20+
|
|
1864
2297
|
AbortSignal.any ? AbortSignal.any([init.signal, controller.signal]) : controller.signal
|
|
1865
2298
|
) : controller.signal;
|
|
1866
|
-
|
|
2299
|
+
try {
|
|
2300
|
+
const response = await fetch(url, { ...init, signal });
|
|
2301
|
+
const hasSessionHeader = init?.headers && new Headers(init.headers).has("mcp-session-id");
|
|
2302
|
+
if (response.status === 404 && hasSessionHeader) {
|
|
2303
|
+
this.client = null;
|
|
2304
|
+
throw new Error("MCP_SESSION_EXPIRED: Downstream session was not found on the server.");
|
|
2305
|
+
}
|
|
2306
|
+
return response;
|
|
2307
|
+
} finally {
|
|
2308
|
+
clearTimeout(timeoutId);
|
|
2309
|
+
}
|
|
1867
2310
|
}
|
|
1868
2311
|
};
|
|
1869
2312
|
if (type === "sse") {
|
|
@@ -1884,17 +2327,18 @@ var MCPClient = class {
|
|
|
1884
2327
|
}
|
|
1885
2328
|
this.emitStateChange("INITIALIZING");
|
|
1886
2329
|
this.emitProgress("Loading session configuration...");
|
|
2330
|
+
let existingSession = null;
|
|
1887
2331
|
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
1888
|
-
|
|
1889
|
-
if (!
|
|
2332
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2333
|
+
if (!existingSession) {
|
|
1890
2334
|
throw new Error(`Session not found: ${this.sessionId}`);
|
|
1891
2335
|
}
|
|
1892
|
-
this.serverUrl = this.serverUrl ||
|
|
1893
|
-
this.callbackUrl = this.callbackUrl ||
|
|
1894
|
-
this.serverName = this.serverName ||
|
|
1895
|
-
this.serverId = this.serverId ||
|
|
1896
|
-
this.headers = this.headers ||
|
|
1897
|
-
this.createdAt =
|
|
2336
|
+
this.serverUrl = this.serverUrl || existingSession.serverUrl;
|
|
2337
|
+
this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
|
|
2338
|
+
this.serverName = this.serverName || existingSession.serverName;
|
|
2339
|
+
this.serverId = this.serverId || existingSession.serverId || "unknown";
|
|
2340
|
+
this.headers = this.headers || existingSession.headers;
|
|
2341
|
+
this.createdAt = existingSession.createdAt;
|
|
1898
2342
|
}
|
|
1899
2343
|
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
1900
2344
|
throw new Error("Missing required connection metadata");
|
|
@@ -1938,10 +2382,13 @@ var MCPClient = class {
|
|
|
1938
2382
|
}
|
|
1939
2383
|
);
|
|
1940
2384
|
}
|
|
1941
|
-
|
|
2385
|
+
if (existingSession === null) {
|
|
2386
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2387
|
+
}
|
|
1942
2388
|
if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
|
|
1943
2389
|
this.createdAt = Date.now();
|
|
1944
|
-
|
|
2390
|
+
const updatedAt = this.createdAt;
|
|
2391
|
+
console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
|
|
1945
2392
|
await sessions.create({
|
|
1946
2393
|
sessionId: this.sessionId,
|
|
1947
2394
|
userId: this.userId,
|
|
@@ -1952,18 +2399,18 @@ var MCPClient = class {
|
|
|
1952
2399
|
transportType: this.transportType || "streamable-http",
|
|
1953
2400
|
headers: this.headers,
|
|
1954
2401
|
createdAt: this.createdAt,
|
|
1955
|
-
|
|
1956
|
-
|
|
2402
|
+
updatedAt,
|
|
2403
|
+
status: "pending"
|
|
2404
|
+
});
|
|
1957
2405
|
}
|
|
1958
2406
|
}
|
|
1959
2407
|
/**
|
|
1960
2408
|
* Saves current session state to the session store
|
|
1961
2409
|
* Creates new session if it doesn't exist, updates if it does
|
|
1962
|
-
* @param
|
|
1963
|
-
* @param active - Session status marker used to avoid unnecessary TTL rewrites
|
|
2410
|
+
* @param status - Session lifecycle status used by storage cleanup
|
|
1964
2411
|
* @private
|
|
1965
2412
|
*/
|
|
1966
|
-
async saveSession(
|
|
2413
|
+
async saveSession(status = "active", existingSession) {
|
|
1967
2414
|
if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
|
|
1968
2415
|
return;
|
|
1969
2416
|
}
|
|
@@ -1977,13 +2424,29 @@ var MCPClient = class {
|
|
|
1977
2424
|
transportType: this.transportType || "streamable-http",
|
|
1978
2425
|
headers: this.headers,
|
|
1979
2426
|
createdAt: this.createdAt || Date.now(),
|
|
1980
|
-
|
|
2427
|
+
updatedAt: Date.now(),
|
|
2428
|
+
status
|
|
1981
2429
|
};
|
|
1982
|
-
|
|
2430
|
+
if (status === "active") {
|
|
2431
|
+
sessionData.authUrl = null;
|
|
2432
|
+
}
|
|
2433
|
+
if (existingSession === void 0) {
|
|
2434
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2435
|
+
}
|
|
1983
2436
|
if (existingSession) {
|
|
1984
|
-
await sessions.update(this.userId, this.sessionId, sessionData
|
|
2437
|
+
await sessions.update(this.userId, this.sessionId, sessionData);
|
|
1985
2438
|
} else {
|
|
1986
|
-
await sessions.create(sessionData
|
|
2439
|
+
await sessions.create(sessionData);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Removes transient setup/auth sessions without masking the original error.
|
|
2444
|
+
* @private
|
|
2445
|
+
*/
|
|
2446
|
+
async deleteTransientSession() {
|
|
2447
|
+
try {
|
|
2448
|
+
await sessions.delete(this.userId, this.sessionId);
|
|
2449
|
+
} catch {
|
|
1987
2450
|
}
|
|
1988
2451
|
}
|
|
1989
2452
|
/**
|
|
@@ -2035,6 +2498,14 @@ var MCPClient = class {
|
|
|
2035
2498
|
* @throws {Error} When connection fails for other reasons
|
|
2036
2499
|
*/
|
|
2037
2500
|
async connect() {
|
|
2501
|
+
if (this.client?.transport) {
|
|
2502
|
+
this.transport = null;
|
|
2503
|
+
try {
|
|
2504
|
+
await this.client.close();
|
|
2505
|
+
} catch {
|
|
2506
|
+
}
|
|
2507
|
+
this.client = null;
|
|
2508
|
+
}
|
|
2038
2509
|
await this.initialize();
|
|
2039
2510
|
if (!this.client || !this.oauthProvider) {
|
|
2040
2511
|
const error = "Client or OAuth provider not initialized";
|
|
@@ -2048,8 +2519,8 @@ var MCPClient = class {
|
|
|
2048
2519
|
this.transportType = transportType;
|
|
2049
2520
|
this.emitStateChange("CONNECTED");
|
|
2050
2521
|
this.emitProgress("Connected successfully");
|
|
2051
|
-
console.log(`[MCPClient] Saving session ${this.sessionId}
|
|
2052
|
-
await this.saveSession(
|
|
2522
|
+
console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
|
|
2523
|
+
await this.saveSession("active");
|
|
2053
2524
|
} catch (error) {
|
|
2054
2525
|
if (error instanceof auth_js.UnauthorizedError || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
|
|
2055
2526
|
let authUrl = "";
|
|
@@ -2061,15 +2532,12 @@ var MCPClient = class {
|
|
|
2061
2532
|
const message = detail.toLowerCase() === "unauthorized" ? "OAuth authorization URL not available" : `OAuth authorization URL not available: ${detail}`;
|
|
2062
2533
|
this.emitError(message, "auth");
|
|
2063
2534
|
this.emitStateChange("FAILED");
|
|
2064
|
-
|
|
2065
|
-
await sessions.delete(this.userId, this.sessionId);
|
|
2066
|
-
} catch {
|
|
2067
|
-
}
|
|
2535
|
+
await this.deleteTransientSession();
|
|
2068
2536
|
throw new Error(message);
|
|
2069
2537
|
}
|
|
2070
2538
|
this.emitStateChange("AUTHENTICATING");
|
|
2071
|
-
console.log(`[MCPClient] Saving session ${this.sessionId}
|
|
2072
|
-
await this.saveSession(
|
|
2539
|
+
console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
|
|
2540
|
+
await this.saveSession("pending");
|
|
2073
2541
|
if (this.serverId) {
|
|
2074
2542
|
this._onConnectionEvent.fire({
|
|
2075
2543
|
type: "auth_required",
|
|
@@ -2089,7 +2557,7 @@ var MCPClient = class {
|
|
|
2089
2557
|
this.emitStateChange("FAILED");
|
|
2090
2558
|
try {
|
|
2091
2559
|
const existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2092
|
-
if (!existingSession || existingSession.
|
|
2560
|
+
if (!existingSession || existingSession.status !== "active") {
|
|
2093
2561
|
await sessions.delete(this.userId, this.sessionId);
|
|
2094
2562
|
}
|
|
2095
2563
|
} catch {
|
|
@@ -2104,7 +2572,7 @@ var MCPClient = class {
|
|
|
2104
2572
|
* @param authCode - Authorization code received from OAuth callback
|
|
2105
2573
|
*/
|
|
2106
2574
|
// TODO: needs to be optimized
|
|
2107
|
-
async finishAuth(authCode) {
|
|
2575
|
+
async finishAuth(authCode, state) {
|
|
2108
2576
|
this.emitStateChange("AUTHENTICATING");
|
|
2109
2577
|
this.emitProgress("Exchanging authorization code for tokens...");
|
|
2110
2578
|
await this.initialize();
|
|
@@ -2114,6 +2582,16 @@ var MCPClient = class {
|
|
|
2114
2582
|
this.emitStateChange("FAILED");
|
|
2115
2583
|
throw new Error(error);
|
|
2116
2584
|
}
|
|
2585
|
+
if (state) {
|
|
2586
|
+
const stateCheck = await this.oauthProvider.checkState(state);
|
|
2587
|
+
if (!stateCheck.valid) {
|
|
2588
|
+
const error = stateCheck.error || "Invalid OAuth state";
|
|
2589
|
+
this.emitError(error, "auth");
|
|
2590
|
+
this.emitStateChange("FAILED");
|
|
2591
|
+
throw new Error(error);
|
|
2592
|
+
}
|
|
2593
|
+
await this.oauthProvider.consumeState(state);
|
|
2594
|
+
}
|
|
2117
2595
|
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2118
2596
|
let lastError;
|
|
2119
2597
|
let tokensExchanged = false;
|
|
@@ -2153,8 +2631,8 @@ var MCPClient = class {
|
|
|
2153
2631
|
await this.client.connect(this.transport);
|
|
2154
2632
|
this.transportType = currentType;
|
|
2155
2633
|
this.emitStateChange("CONNECTED");
|
|
2156
|
-
console.log(`[MCPClient]
|
|
2157
|
-
await this.saveSession(
|
|
2634
|
+
console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
|
|
2635
|
+
await this.saveSession("active");
|
|
2158
2636
|
return;
|
|
2159
2637
|
} catch (error) {
|
|
2160
2638
|
lastError = error;
|
|
@@ -2167,12 +2645,14 @@ var MCPClient = class {
|
|
|
2167
2645
|
const msg = error instanceof Error ? error.message : "Authentication failed";
|
|
2168
2646
|
this.emitError(msg, "auth");
|
|
2169
2647
|
this.emitStateChange("FAILED");
|
|
2648
|
+
await this.deleteTransientSession();
|
|
2170
2649
|
throw error;
|
|
2171
2650
|
}
|
|
2172
2651
|
if (isLastAttempt) {
|
|
2173
2652
|
const msg = error instanceof Error ? error.message : "Authentication failed";
|
|
2174
2653
|
this.emitError(msg, "auth");
|
|
2175
2654
|
this.emitStateChange("FAILED");
|
|
2655
|
+
await this.deleteTransientSession();
|
|
2176
2656
|
throw error;
|
|
2177
2657
|
}
|
|
2178
2658
|
this.emitProgress(`Auth attempt with ${currentType} failed: ${errorMessage}. Retrying...`);
|
|
@@ -2182,6 +2662,7 @@ var MCPClient = class {
|
|
|
2182
2662
|
const errorMessage = lastError instanceof Error ? lastError.message : "Authentication failed";
|
|
2183
2663
|
this.emitError(errorMessage, "auth");
|
|
2184
2664
|
this.emitStateChange("FAILED");
|
|
2665
|
+
await this.deleteTransientSession();
|
|
2185
2666
|
throw lastError;
|
|
2186
2667
|
}
|
|
2187
2668
|
}
|
|
@@ -2403,7 +2884,7 @@ var MCPClient = class {
|
|
|
2403
2884
|
await this.oauthProvider.invalidateCredentials("all");
|
|
2404
2885
|
}
|
|
2405
2886
|
await sessions.delete(this.userId, this.sessionId);
|
|
2406
|
-
this.disconnect();
|
|
2887
|
+
await this.disconnect();
|
|
2407
2888
|
}
|
|
2408
2889
|
/**
|
|
2409
2890
|
* Checks if the client is currently connected to an MCP server
|
|
@@ -2413,10 +2894,21 @@ var MCPClient = class {
|
|
|
2413
2894
|
return this.client !== null;
|
|
2414
2895
|
}
|
|
2415
2896
|
/**
|
|
2416
|
-
* Disconnects from the MCP server and cleans up resources
|
|
2417
|
-
* Does not remove session from Redis
|
|
2897
|
+
* Disconnects from the MCP server and cleans up resources.
|
|
2898
|
+
* Does not remove session from Redis — use clearSession() for that.
|
|
2899
|
+
*
|
|
2900
|
+
* For Streamable HTTP sessions, sends an HTTP DELETE to the MCP endpoint
|
|
2901
|
+
* before closing, as recommended by the MCP Streamable HTTP spec
|
|
2902
|
+
* (section "Session Management", rule 5). This is best-effort — errors
|
|
2903
|
+
* (e.g. server already restarted, 404/405 responses) are silently ignored.
|
|
2418
2904
|
*/
|
|
2419
|
-
disconnect(
|
|
2905
|
+
async disconnect() {
|
|
2906
|
+
if (this.transport instanceof streamableHttp_js.StreamableHTTPClientTransport) {
|
|
2907
|
+
try {
|
|
2908
|
+
await this.transport.terminateSession();
|
|
2909
|
+
} catch {
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2420
2912
|
if (this.client) {
|
|
2421
2913
|
this.client.close();
|
|
2422
2914
|
}
|
|
@@ -2428,7 +2920,6 @@ var MCPClient = class {
|
|
|
2428
2920
|
type: "disconnected",
|
|
2429
2921
|
sessionId: this.sessionId,
|
|
2430
2922
|
serverId: this.serverId,
|
|
2431
|
-
reason,
|
|
2432
2923
|
timestamp: Date.now()
|
|
2433
2924
|
});
|
|
2434
2925
|
this._onObservabilityEvent.fire({
|
|
@@ -2437,9 +2928,7 @@ var MCPClient = class {
|
|
|
2437
2928
|
message: `Disconnected from ${this.serverId}`,
|
|
2438
2929
|
sessionId: this.sessionId,
|
|
2439
2930
|
serverId: this.serverId,
|
|
2440
|
-
payload: {
|
|
2441
|
-
reason: reason || "unknown"
|
|
2442
|
-
},
|
|
2931
|
+
payload: {},
|
|
2443
2932
|
timestamp: Date.now(),
|
|
2444
2933
|
id: nanoid.nanoid()
|
|
2445
2934
|
});
|
|
@@ -2496,48 +2985,6 @@ var MCPClient = class {
|
|
|
2496
2985
|
getSessionId() {
|
|
2497
2986
|
return this.sessionId;
|
|
2498
2987
|
}
|
|
2499
|
-
/**
|
|
2500
|
-
* Gets MCP server configuration for all active user sessions
|
|
2501
|
-
* Loads sessions from storage and returns server connection metadata.
|
|
2502
|
-
* OAuth refresh is handled by SDK transports through their authProvider.
|
|
2503
|
-
* @deprecated This returns legacy connection metadata only and does not
|
|
2504
|
-
* include OAuth tokens or generated Authorization headers. Prefer
|
|
2505
|
-
* MultiSessionClient or explicit MCPClient instances so SDK transports can
|
|
2506
|
-
* own OAuth refresh and reauthorization.
|
|
2507
|
-
* @param userId - User ID to fetch sessions for
|
|
2508
|
-
* @returns Object keyed by sanitized server labels containing transport and url.
|
|
2509
|
-
* @static
|
|
2510
|
-
*/
|
|
2511
|
-
static async getMcpServerConfig(userId) {
|
|
2512
|
-
const mcpConfig = {};
|
|
2513
|
-
const sessionList = await sessions.list(userId);
|
|
2514
|
-
await Promise.all(
|
|
2515
|
-
sessionList.map(async (sessionData) => {
|
|
2516
|
-
const { sessionId } = sessionData;
|
|
2517
|
-
try {
|
|
2518
|
-
if (!sessionData.serverId || !sessionData.transportType || !sessionData.serverUrl || !sessionData.callbackUrl) {
|
|
2519
|
-
await sessions.delete(userId, sessionId);
|
|
2520
|
-
return;
|
|
2521
|
-
}
|
|
2522
|
-
const label = sanitizeServerLabel(
|
|
2523
|
-
sessionData.serverName || sessionData.serverId || "server"
|
|
2524
|
-
);
|
|
2525
|
-
mcpConfig[label] = {
|
|
2526
|
-
transport: sessionData.transportType,
|
|
2527
|
-
url: sessionData.serverUrl,
|
|
2528
|
-
...sessionData.serverName && {
|
|
2529
|
-
serverName: sessionData.serverName,
|
|
2530
|
-
serverLabel: label
|
|
2531
|
-
}
|
|
2532
|
-
};
|
|
2533
|
-
} catch (error) {
|
|
2534
|
-
await sessions.delete(userId, sessionId);
|
|
2535
|
-
console.warn(`[MCP] Failed to process session ${sessionId}:`, error);
|
|
2536
|
-
}
|
|
2537
|
-
})
|
|
2538
|
-
);
|
|
2539
|
-
return mcpConfig;
|
|
2540
|
-
}
|
|
2541
2988
|
};
|
|
2542
2989
|
|
|
2543
2990
|
// src/server/mcp/multi-session-client.ts
|
|
@@ -2573,18 +3020,13 @@ var MultiSessionClient = class {
|
|
|
2573
3020
|
*
|
|
2574
3021
|
* A session is considered connectable when:
|
|
2575
3022
|
* - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
|
|
2576
|
-
* - Its
|
|
2577
|
-
* either mid-OAuth flow, auth-pending, or previously failed. We skip those here
|
|
3023
|
+
* - Its status is `active`. Pending sessions are skipped here
|
|
2578
3024
|
* and let the OAuth flow complete separately before we try to reconnect them.
|
|
2579
|
-
*
|
|
2580
|
-
* Note: Sessions where `active` is `undefined` (legacy records) are included
|
|
2581
|
-
* for backwards compatibility.
|
|
2582
3025
|
*/
|
|
2583
3026
|
async getActiveSessions() {
|
|
2584
3027
|
const sessionList = await sessions.list(this.userId);
|
|
2585
3028
|
const valid = sessionList.filter(
|
|
2586
|
-
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.
|
|
2587
|
-
// exclude OAuth-pending / failed sessions
|
|
3029
|
+
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
|
|
2588
3030
|
);
|
|
2589
3031
|
return valid;
|
|
2590
3032
|
}
|
|
@@ -2605,6 +3047,9 @@ var MultiSessionClient = class {
|
|
|
2605
3047
|
* Connects a single session, with built-in deduplication to prevent race conditions.
|
|
2606
3048
|
*
|
|
2607
3049
|
* - If a client for this session already exists and is connected, returns immediately.
|
|
3050
|
+
* - If the existing client entry is no longer connected (e.g. it was explicitly
|
|
3051
|
+
* disconnected), it is evicted so that `establishConnectionWithRetries` creates a
|
|
3052
|
+
* fresh transport — preventing "Client already connected" errors from the SDK.
|
|
2608
3053
|
* - If a connection attempt for this session is already in-flight (e.g. from a
|
|
2609
3054
|
* concurrent call), it joins the existing promise instead of starting a new one.
|
|
2610
3055
|
* This is the key concurrency lock — the `connectionPromises` map acts as a
|
|
@@ -2612,9 +3057,12 @@ var MultiSessionClient = class {
|
|
|
2612
3057
|
* - On completion (success or failure), the promise is cleaned up from the map.
|
|
2613
3058
|
*/
|
|
2614
3059
|
async connectSession(session) {
|
|
2615
|
-
const
|
|
2616
|
-
if (
|
|
2617
|
-
|
|
3060
|
+
const existing = this.clients.find((c) => c.getSessionId() === session.sessionId);
|
|
3061
|
+
if (existing) {
|
|
3062
|
+
if (existing.isConnected()) {
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
this.clients = this.clients.filter((c) => c !== existing);
|
|
2618
3066
|
}
|
|
2619
3067
|
if (this.connectionPromises.has(session.sessionId)) {
|
|
2620
3068
|
return this.connectionPromises.get(session.sessionId);
|
|
@@ -2689,8 +3137,24 @@ var MultiSessionClient = class {
|
|
|
2689
3137
|
*/
|
|
2690
3138
|
async connect() {
|
|
2691
3139
|
const sessions2 = await this.getActiveSessions();
|
|
3140
|
+
const activeSessionIds = new Set(sessions2.map((s) => s.sessionId));
|
|
3141
|
+
this.clients = this.clients.filter((c) => activeSessionIds.has(c.getSessionId()));
|
|
2692
3142
|
await this.connectInBatches(sessions2);
|
|
2693
3143
|
}
|
|
3144
|
+
/**
|
|
3145
|
+
* Drops all cached `MCPClient` instances and reconnects fresh from storage.
|
|
3146
|
+
*
|
|
3147
|
+
* Call this when downstream MCP servers have expired their transport sessions
|
|
3148
|
+
* (e.g. after a remote server restart) and subsequent tool calls return
|
|
3149
|
+
* "Session not found. Reconnect without session header." errors.
|
|
3150
|
+
*
|
|
3151
|
+
* OAuth tokens are preserved in the storage backend — no re-authentication
|
|
3152
|
+
* is required. Only the in-memory transport sessions are cleared.
|
|
3153
|
+
*/
|
|
3154
|
+
async reconnect() {
|
|
3155
|
+
await this.disconnect();
|
|
3156
|
+
await this.connect();
|
|
3157
|
+
}
|
|
2694
3158
|
/**
|
|
2695
3159
|
* Returns all currently connected `MCPClient` instances.
|
|
2696
3160
|
*
|
|
@@ -2703,11 +3167,15 @@ var MultiSessionClient = class {
|
|
|
2703
3167
|
/**
|
|
2704
3168
|
* Gracefully disconnects all active MCP clients and clears the internal client list.
|
|
2705
3169
|
*
|
|
3170
|
+
* For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
|
|
3171
|
+
* endpoint per the spec before closing locally. All disconnects run in
|
|
3172
|
+
* parallel so shutdown is not serialised across many sessions.
|
|
3173
|
+
*
|
|
2706
3174
|
* Call this during server shutdown or when a user logs out to free up
|
|
2707
3175
|
* underlying transport resources (SSE streams, HTTP connections, etc.).
|
|
2708
3176
|
*/
|
|
2709
|
-
disconnect() {
|
|
2710
|
-
this.clients.
|
|
3177
|
+
async disconnect() {
|
|
3178
|
+
await Promise.all(this.clients.map((client) => client.disconnect()));
|
|
2711
3179
|
this.clients = [];
|
|
2712
3180
|
}
|
|
2713
3181
|
};
|
|
@@ -2859,7 +3327,8 @@ var SSEConnectionManager = class {
|
|
|
2859
3327
|
serverUrl: s.serverUrl,
|
|
2860
3328
|
transport: s.transportType,
|
|
2861
3329
|
createdAt: s.createdAt,
|
|
2862
|
-
|
|
3330
|
+
updatedAt: s.updatedAt ?? s.createdAt,
|
|
3331
|
+
status: s.status ?? "pending"
|
|
2863
3332
|
}))
|
|
2864
3333
|
};
|
|
2865
3334
|
}
|
|
@@ -2875,7 +3344,7 @@ var SSEConnectionManager = class {
|
|
|
2875
3344
|
(s) => s.serverId === serverId || s.serverUrl === serverUrl
|
|
2876
3345
|
);
|
|
2877
3346
|
if (duplicate) {
|
|
2878
|
-
if (duplicate.
|
|
3347
|
+
if (duplicate.status === "pending") {
|
|
2879
3348
|
await this.getSession({ sessionId: duplicate.sessionId });
|
|
2880
3349
|
return {
|
|
2881
3350
|
sessionId: duplicate.sessionId,
|
|
@@ -2940,7 +3409,6 @@ var SSEConnectionManager = class {
|
|
|
2940
3409
|
const client = this.clients.get(sessionId);
|
|
2941
3410
|
if (client) {
|
|
2942
3411
|
await client.clearSession();
|
|
2943
|
-
client.disconnect();
|
|
2944
3412
|
this.clients.delete(sessionId);
|
|
2945
3413
|
} else {
|
|
2946
3414
|
await sessions.delete(this.userId, sessionId);
|
|
@@ -3062,7 +3530,10 @@ var SSEConnectionManager = class {
|
|
|
3062
3530
|
* Complete OAuth authorization flow
|
|
3063
3531
|
*/
|
|
3064
3532
|
async finishAuth(params) {
|
|
3065
|
-
const {
|
|
3533
|
+
const { code } = params;
|
|
3534
|
+
const oauthState = params.state;
|
|
3535
|
+
const parsedState = parseOAuthState(oauthState);
|
|
3536
|
+
const sessionId = parsedState?.sessionId || oauthState;
|
|
3066
3537
|
const session = await sessions.get(this.userId, sessionId);
|
|
3067
3538
|
if (!session) {
|
|
3068
3539
|
throw new Error("Session not found");
|
|
@@ -3087,7 +3558,7 @@ var SSEConnectionManager = class {
|
|
|
3087
3558
|
headers: session.headers
|
|
3088
3559
|
});
|
|
3089
3560
|
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
3090
|
-
await client.finishAuth(code);
|
|
3561
|
+
await client.finishAuth(code, oauthState);
|
|
3091
3562
|
this.clients.set(sessionId, client);
|
|
3092
3563
|
const tools = await client.listTools();
|
|
3093
3564
|
return { success: true, toolCount: tools.tools.length };
|
|
@@ -3146,14 +3617,14 @@ var SSEConnectionManager = class {
|
|
|
3146
3617
|
/**
|
|
3147
3618
|
* Cleanup and close all connections
|
|
3148
3619
|
*/
|
|
3149
|
-
dispose() {
|
|
3620
|
+
async dispose() {
|
|
3150
3621
|
this.isActive = false;
|
|
3151
3622
|
if (this.heartbeatTimer) {
|
|
3152
3623
|
clearInterval(this.heartbeatTimer);
|
|
3153
3624
|
}
|
|
3154
|
-
|
|
3155
|
-
client.disconnect()
|
|
3156
|
-
|
|
3625
|
+
await Promise.all(
|
|
3626
|
+
Array.from(this.clients.values()).map((client) => client.disconnect())
|
|
3627
|
+
);
|
|
3157
3628
|
this.clients.clear();
|
|
3158
3629
|
}
|
|
3159
3630
|
};
|