@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/index.js
CHANGED
|
@@ -160,24 +160,9 @@ init_cjs_shims();
|
|
|
160
160
|
// src/server/storage/redis-backend.ts
|
|
161
161
|
init_cjs_shims();
|
|
162
162
|
|
|
163
|
-
// src/shared/constants.ts
|
|
164
|
-
init_cjs_shims();
|
|
165
|
-
var SESSION_TTL_SECONDS = 43200;
|
|
166
|
-
var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
|
|
167
|
-
var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
168
|
-
var REDIS_KEY_PREFIX = "mcp:session:";
|
|
169
|
-
var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
170
|
-
var DEFAULT_CLIENT_NAME = "MCP Assistant";
|
|
171
|
-
var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
|
|
172
|
-
var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
|
|
173
|
-
var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
|
|
174
|
-
var SOFTWARE_ID = "@mcp-ts";
|
|
175
|
-
var SOFTWARE_VERSION = "1.3.4";
|
|
176
|
-
var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
|
|
177
|
-
var MCP_CLIENT_VERSION = "2.0";
|
|
178
|
-
|
|
179
163
|
// src/shared/utils.ts
|
|
180
164
|
init_cjs_shims();
|
|
165
|
+
var OAUTH_STATE_SEPARATOR = ".";
|
|
181
166
|
var firstChar = nanoid.customAlphabet(
|
|
182
167
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
|
183
168
|
1
|
|
@@ -196,13 +181,102 @@ function sanitizeServerLabel(name) {
|
|
|
196
181
|
function generateSessionId() {
|
|
197
182
|
return firstChar() + rest();
|
|
198
183
|
}
|
|
184
|
+
function formatOAuthState(nonce, sessionId) {
|
|
185
|
+
return `${nonce}${OAUTH_STATE_SEPARATOR}${sessionId}`;
|
|
186
|
+
}
|
|
187
|
+
function parseOAuthState(state) {
|
|
188
|
+
const separatorIndex = state.indexOf(OAUTH_STATE_SEPARATOR);
|
|
189
|
+
if (separatorIndex <= 0 || separatorIndex === state.length - 1) {
|
|
190
|
+
return void 0;
|
|
191
|
+
}
|
|
192
|
+
const nonce = state.slice(0, separatorIndex);
|
|
193
|
+
const sessionId = state.slice(separatorIndex + 1);
|
|
194
|
+
if (!nonce || !sessionId || sessionId.includes(OAUTH_STATE_SEPARATOR)) {
|
|
195
|
+
return void 0;
|
|
196
|
+
}
|
|
197
|
+
return { nonce, sessionId };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/server/storage/session-lifecycle.ts
|
|
201
|
+
init_cjs_shims();
|
|
202
|
+
|
|
203
|
+
// src/shared/constants.ts
|
|
204
|
+
init_cjs_shims();
|
|
205
|
+
var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
|
|
206
|
+
var PENDING_SESSION_EXPIRATION_SECONDS = Math.floor(STATE_EXPIRATION_MS / 1e3);
|
|
207
|
+
var DORMANT_SESSION_EXPIRATION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
208
|
+
var DORMANT_SESSION_EXPIRATION_SECONDS = Math.floor(DORMANT_SESSION_EXPIRATION_MS / 1e3);
|
|
209
|
+
var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
210
|
+
var REDIS_KEY_PREFIX = "mcp:session:";
|
|
211
|
+
var DEFAULT_CLIENT_NAME = "MCP Assistant";
|
|
212
|
+
var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
|
|
213
|
+
var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
|
|
214
|
+
var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
|
|
215
|
+
var SOFTWARE_ID = "@mcp-ts";
|
|
216
|
+
var SOFTWARE_VERSION = "2.3.4";
|
|
217
|
+
var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
|
|
218
|
+
var MCP_CLIENT_VERSION = "2.0";
|
|
219
|
+
|
|
220
|
+
// src/server/storage/session-lifecycle.ts
|
|
221
|
+
function resolveSessionExpiresAt(status = "pending", referenceTime = Date.now()) {
|
|
222
|
+
return status === "active" ? null : referenceTime + STATE_EXPIRATION_MS;
|
|
223
|
+
}
|
|
224
|
+
function resolveSessionRedisTtlSeconds(session) {
|
|
225
|
+
return session.status === "active" ? DORMANT_SESSION_EXPIRATION_SECONDS : Math.floor(STATE_EXPIRATION_MS / 1e3);
|
|
226
|
+
}
|
|
227
|
+
function normalizeNewSession(session, now = Date.now()) {
|
|
228
|
+
const createdAt = session.createdAt || now;
|
|
229
|
+
const updatedAt = session.updatedAt ?? createdAt;
|
|
230
|
+
const status = session.status ?? "pending";
|
|
231
|
+
return {
|
|
232
|
+
...session,
|
|
233
|
+
status,
|
|
234
|
+
createdAt,
|
|
235
|
+
updatedAt,
|
|
236
|
+
expiresAt: resolveSessionExpiresAt(status, status === "active" ? updatedAt : createdAt)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function mergeSessionUpdate(current, data, now = Date.now()) {
|
|
240
|
+
const updatedAt = data.updatedAt ?? now;
|
|
241
|
+
const updated = {
|
|
242
|
+
...current,
|
|
243
|
+
...data,
|
|
244
|
+
updatedAt
|
|
245
|
+
};
|
|
246
|
+
const status = updated.status ?? "pending";
|
|
247
|
+
return {
|
|
248
|
+
...updated,
|
|
249
|
+
status,
|
|
250
|
+
expiresAt: resolveSessionExpiresAt(status, updatedAt)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function normalizeStoredSession(session) {
|
|
254
|
+
const createdAt = session.createdAt || Date.now();
|
|
255
|
+
const updatedAt = session.updatedAt ?? createdAt;
|
|
256
|
+
const status = session.status ?? "pending";
|
|
257
|
+
const expiresAt = status === "active" ? null : session.expiresAt ?? resolveSessionExpiresAt(status, createdAt);
|
|
258
|
+
return {
|
|
259
|
+
...session,
|
|
260
|
+
status,
|
|
261
|
+
createdAt,
|
|
262
|
+
updatedAt,
|
|
263
|
+
expiresAt
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function isSessionExpired(session, now = Date.now()) {
|
|
267
|
+
const hydrated = normalizeStoredSession(session);
|
|
268
|
+
if (hydrated.status === "active") {
|
|
269
|
+
return hydrated.updatedAt !== void 0 && hydrated.updatedAt < now - DORMANT_SESSION_EXPIRATION_MS;
|
|
270
|
+
}
|
|
271
|
+
return typeof hydrated.expiresAt === "number" && hydrated.expiresAt < now;
|
|
272
|
+
}
|
|
199
273
|
|
|
200
274
|
// src/server/storage/redis-backend.ts
|
|
201
275
|
var RedisStorageBackend = class {
|
|
202
276
|
constructor(redis2) {
|
|
203
277
|
this.redis = redis2;
|
|
204
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
205
278
|
__publicField(this, "KEY_PREFIX", "mcp:session:");
|
|
279
|
+
__publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
|
|
206
280
|
__publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
|
|
207
281
|
__publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
|
|
208
282
|
}
|
|
@@ -221,6 +295,9 @@ var RedisStorageBackend = class {
|
|
|
221
295
|
getSessionKey(userId, sessionId) {
|
|
222
296
|
return `${this.KEY_PREFIX}${userId}:${sessionId}`;
|
|
223
297
|
}
|
|
298
|
+
getCredentialsKey(userId, sessionId) {
|
|
299
|
+
return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
|
|
300
|
+
}
|
|
224
301
|
/**
|
|
225
302
|
* Generates Redis key for tracking all sessions for a user
|
|
226
303
|
* @private
|
|
@@ -258,15 +335,16 @@ var RedisStorageBackend = class {
|
|
|
258
335
|
generateSessionId() {
|
|
259
336
|
return generateSessionId();
|
|
260
337
|
}
|
|
261
|
-
async create(session
|
|
338
|
+
async create(session) {
|
|
262
339
|
const { sessionId, userId } = session;
|
|
263
340
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
264
341
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
265
342
|
const userIdKey = this.getUserIdKey(userId);
|
|
266
|
-
const
|
|
343
|
+
const sessionWithLifecycle = normalizeNewSession(session);
|
|
344
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(sessionWithLifecycle);
|
|
267
345
|
const result = await this.redis.set(
|
|
268
346
|
sessionKey,
|
|
269
|
-
JSON.stringify(
|
|
347
|
+
JSON.stringify(sessionWithLifecycle),
|
|
270
348
|
"EX",
|
|
271
349
|
effectiveTtl,
|
|
272
350
|
"NX"
|
|
@@ -276,9 +354,8 @@ var RedisStorageBackend = class {
|
|
|
276
354
|
}
|
|
277
355
|
await this.redis.sadd(userIdKey, sessionId);
|
|
278
356
|
}
|
|
279
|
-
async update(userId, sessionId, data
|
|
357
|
+
async update(userId, sessionId, data) {
|
|
280
358
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
281
|
-
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
282
359
|
const script = `
|
|
283
360
|
local currentStr = redis.call("GET", KEYS[1])
|
|
284
361
|
if not currentStr then
|
|
@@ -286,26 +363,47 @@ var RedisStorageBackend = class {
|
|
|
286
363
|
end
|
|
287
364
|
|
|
288
365
|
local current = cjson.decode(currentStr)
|
|
289
|
-
local
|
|
290
|
-
|
|
291
|
-
for k,v in pairs(updates) do
|
|
292
|
-
current[k] = v
|
|
293
|
-
end
|
|
366
|
+
local updated = cjson.decode(ARGV[1])
|
|
294
367
|
|
|
295
|
-
redis.call("SET", KEYS[1], cjson.encode(
|
|
368
|
+
redis.call("SET", KEYS[1], cjson.encode(updated), "EX", ARGV[2])
|
|
296
369
|
return 1
|
|
297
370
|
`;
|
|
371
|
+
const current = await this.get(userId, sessionId);
|
|
372
|
+
if (!current) {
|
|
373
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
374
|
+
}
|
|
375
|
+
const updated = mergeSessionUpdate(current, data);
|
|
376
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
|
|
298
377
|
const result = await this.redis.eval(
|
|
299
378
|
script,
|
|
300
379
|
1,
|
|
301
380
|
sessionKey,
|
|
302
|
-
JSON.stringify(
|
|
381
|
+
JSON.stringify(updated),
|
|
303
382
|
effectiveTtl
|
|
304
383
|
);
|
|
305
384
|
if (result === 0) {
|
|
306
385
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
307
386
|
}
|
|
308
387
|
}
|
|
388
|
+
async patchCredentials(userId, sessionId, data) {
|
|
389
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
390
|
+
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
391
|
+
const sessionExists = await this.redis.exists(sessionKey);
|
|
392
|
+
if (!sessionExists) {
|
|
393
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
394
|
+
}
|
|
395
|
+
const session = await this.get(userId, sessionId);
|
|
396
|
+
const currentTtl = await this.redis.ttl(sessionKey);
|
|
397
|
+
const effectiveTtl = currentTtl > 0 && session ? currentTtl : resolveSessionRedisTtlSeconds(session ?? { status: "pending" });
|
|
398
|
+
const currentStr = await this.redis.get(credentialsKey);
|
|
399
|
+
const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
|
|
400
|
+
const credentials = { ...current, ...data, sessionId, userId };
|
|
401
|
+
if (effectiveTtl > 0) {
|
|
402
|
+
await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
|
|
403
|
+
} else {
|
|
404
|
+
await this.redis.set(credentialsKey, JSON.stringify(credentials));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
309
407
|
async get(userId, sessionId) {
|
|
310
408
|
try {
|
|
311
409
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
@@ -313,13 +411,28 @@ var RedisStorageBackend = class {
|
|
|
313
411
|
if (!sessionDataStr) {
|
|
314
412
|
return null;
|
|
315
413
|
}
|
|
316
|
-
const
|
|
317
|
-
return
|
|
414
|
+
const session = JSON.parse(sessionDataStr);
|
|
415
|
+
return normalizeStoredSession(session);
|
|
318
416
|
} catch (error) {
|
|
319
417
|
console.error("[RedisStorageBackend] Failed to get session:", error);
|
|
320
418
|
return null;
|
|
321
419
|
}
|
|
322
420
|
}
|
|
421
|
+
async getCredentials(userId, sessionId) {
|
|
422
|
+
const session = await this.get(userId, sessionId);
|
|
423
|
+
if (!session) return null;
|
|
424
|
+
const credentialsStr = await this.redis.get(this.getCredentialsKey(userId, sessionId));
|
|
425
|
+
return credentialsStr ? JSON.parse(credentialsStr) : { sessionId, userId };
|
|
426
|
+
}
|
|
427
|
+
async clearCredentials(userId, sessionId) {
|
|
428
|
+
await this.patchCredentials(userId, sessionId, {
|
|
429
|
+
clientInformation: null,
|
|
430
|
+
tokens: null,
|
|
431
|
+
codeVerifier: null,
|
|
432
|
+
clientId: null,
|
|
433
|
+
oauthState: null
|
|
434
|
+
});
|
|
435
|
+
}
|
|
323
436
|
async listIds(userId) {
|
|
324
437
|
const sessions2 = await this.list(userId);
|
|
325
438
|
return sessions2.map((session) => session.sessionId);
|
|
@@ -339,7 +452,7 @@ var RedisStorageBackend = class {
|
|
|
339
452
|
if (staleSessionIds.length > 0) {
|
|
340
453
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
341
454
|
}
|
|
342
|
-
return results.filter((session) => session !== null);
|
|
455
|
+
return results.filter((session) => session !== null).map((session) => normalizeStoredSession(session));
|
|
343
456
|
} catch (error) {
|
|
344
457
|
console.error(`[RedisStorageBackend] Failed to get session data for ${userId}:`, error);
|
|
345
458
|
return [];
|
|
@@ -348,9 +461,10 @@ var RedisStorageBackend = class {
|
|
|
348
461
|
async delete(userId, sessionId) {
|
|
349
462
|
try {
|
|
350
463
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
464
|
+
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
351
465
|
const userIdKey = this.getUserIdKey(userId);
|
|
352
466
|
await this.redis.srem(userIdKey, sessionId);
|
|
353
|
-
await this.redis.del(sessionKey);
|
|
467
|
+
await this.redis.del(sessionKey, credentialsKey);
|
|
354
468
|
} catch (error) {
|
|
355
469
|
console.error("[RedisStorageBackend] Failed to remove session:", error);
|
|
356
470
|
}
|
|
@@ -365,7 +479,7 @@ var RedisStorageBackend = class {
|
|
|
365
479
|
return null;
|
|
366
480
|
}
|
|
367
481
|
try {
|
|
368
|
-
return JSON.parse(data).sessionId;
|
|
482
|
+
return normalizeStoredSession(JSON.parse(data)).sessionId;
|
|
369
483
|
} catch (error) {
|
|
370
484
|
console.error("[RedisStorageBackend] Failed to parse session while listing all session IDs:", error);
|
|
371
485
|
return null;
|
|
@@ -381,8 +495,9 @@ var RedisStorageBackend = class {
|
|
|
381
495
|
async clearAll() {
|
|
382
496
|
try {
|
|
383
497
|
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
498
|
+
const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
|
|
384
499
|
const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
|
|
385
|
-
const allKeys = [...keys, ...userIdKeys];
|
|
500
|
+
const allKeys = [...keys, ...credentialKeys, ...userIdKeys];
|
|
386
501
|
if (allKeys.length > 0) {
|
|
387
502
|
await this.redis.del(...allKeys);
|
|
388
503
|
}
|
|
@@ -406,6 +521,7 @@ var RedisStorageBackend = class {
|
|
|
406
521
|
const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
|
|
407
522
|
if (staleSessionIds.length > 0) {
|
|
408
523
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
524
|
+
await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
|
|
409
525
|
}
|
|
410
526
|
const remainingCount = await this.redis.scard(userIdKey);
|
|
411
527
|
if (remainingCount === 0) {
|
|
@@ -431,6 +547,7 @@ var MemoryStorageBackend = class {
|
|
|
431
547
|
constructor() {
|
|
432
548
|
// Map<userId:sessionId, Session>
|
|
433
549
|
__publicField(this, "sessions", /* @__PURE__ */ new Map());
|
|
550
|
+
__publicField(this, "credentials", /* @__PURE__ */ new Map());
|
|
434
551
|
// Map<userId, Set<sessionId>>
|
|
435
552
|
__publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
|
|
436
553
|
}
|
|
@@ -443,36 +560,55 @@ var MemoryStorageBackend = class {
|
|
|
443
560
|
generateSessionId() {
|
|
444
561
|
return generateSessionId();
|
|
445
562
|
}
|
|
446
|
-
async create(session
|
|
563
|
+
async create(session) {
|
|
447
564
|
const { sessionId, userId } = session;
|
|
448
565
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
449
566
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
450
567
|
if (this.sessions.has(sessionKey)) {
|
|
451
568
|
throw new Error(`Session ${sessionId} already exists`);
|
|
452
569
|
}
|
|
453
|
-
this.sessions.set(sessionKey, session);
|
|
570
|
+
this.sessions.set(sessionKey, normalizeNewSession(session));
|
|
454
571
|
if (!this.userIdSessions.has(userId)) {
|
|
455
572
|
this.userIdSessions.set(userId, /* @__PURE__ */ new Set());
|
|
456
573
|
}
|
|
457
574
|
this.userIdSessions.get(userId).add(sessionId);
|
|
458
575
|
}
|
|
459
|
-
async update(userId, sessionId, data
|
|
576
|
+
async update(userId, sessionId, data) {
|
|
460
577
|
if (!userId || !sessionId) throw new Error("userId and sessionId required");
|
|
461
578
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
462
579
|
const current = this.sessions.get(sessionKey);
|
|
463
580
|
if (!current) {
|
|
464
581
|
throw new Error(`Session ${sessionId} not found`);
|
|
465
582
|
}
|
|
466
|
-
const updated =
|
|
467
|
-
...current,
|
|
468
|
-
...data
|
|
469
|
-
};
|
|
583
|
+
const updated = mergeSessionUpdate(current, data);
|
|
470
584
|
this.sessions.set(sessionKey, updated);
|
|
471
585
|
}
|
|
586
|
+
async patchCredentials(userId, sessionId, data) {
|
|
587
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
588
|
+
if (!this.sessions.has(sessionKey)) {
|
|
589
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
590
|
+
}
|
|
591
|
+
const current = this.credentials.get(sessionKey) ?? { sessionId, userId };
|
|
592
|
+
this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
593
|
+
}
|
|
472
594
|
async get(userId, sessionId) {
|
|
473
595
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
474
596
|
return this.sessions.get(sessionKey) || null;
|
|
475
597
|
}
|
|
598
|
+
async getCredentials(userId, sessionId) {
|
|
599
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
600
|
+
if (!this.sessions.has(sessionKey)) return null;
|
|
601
|
+
return this.credentials.get(sessionKey) ?? { sessionId, userId };
|
|
602
|
+
}
|
|
603
|
+
async clearCredentials(userId, sessionId) {
|
|
604
|
+
await this.patchCredentials(userId, sessionId, {
|
|
605
|
+
clientInformation: null,
|
|
606
|
+
tokens: null,
|
|
607
|
+
codeVerifier: null,
|
|
608
|
+
clientId: null,
|
|
609
|
+
oauthState: null
|
|
610
|
+
});
|
|
611
|
+
}
|
|
476
612
|
async listIds(userId) {
|
|
477
613
|
const set = this.userIdSessions.get(userId);
|
|
478
614
|
return set ? Array.from(set) : [];
|
|
@@ -492,6 +628,7 @@ var MemoryStorageBackend = class {
|
|
|
492
628
|
async delete(userId, sessionId) {
|
|
493
629
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
494
630
|
this.sessions.delete(sessionKey);
|
|
631
|
+
this.credentials.delete(sessionKey);
|
|
495
632
|
const set = this.userIdSessions.get(userId);
|
|
496
633
|
if (set) {
|
|
497
634
|
set.delete(sessionId);
|
|
@@ -505,9 +642,22 @@ var MemoryStorageBackend = class {
|
|
|
505
642
|
}
|
|
506
643
|
async clearAll() {
|
|
507
644
|
this.sessions.clear();
|
|
645
|
+
this.credentials.clear();
|
|
508
646
|
this.userIdSessions.clear();
|
|
509
647
|
}
|
|
510
648
|
async cleanupExpired() {
|
|
649
|
+
for (const [key, session] of this.sessions.entries()) {
|
|
650
|
+
if (!isSessionExpired(session)) continue;
|
|
651
|
+
this.sessions.delete(key);
|
|
652
|
+
this.credentials.delete(key);
|
|
653
|
+
const set = this.userIdSessions.get(session.userId);
|
|
654
|
+
if (set) {
|
|
655
|
+
set.delete(session.sessionId);
|
|
656
|
+
if (set.size === 0) {
|
|
657
|
+
this.userIdSessions.delete(session.userId);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
511
661
|
}
|
|
512
662
|
async disconnect() {
|
|
513
663
|
}
|
|
@@ -522,6 +672,7 @@ var FileStorageBackend = class {
|
|
|
522
672
|
constructor(options = {}) {
|
|
523
673
|
__publicField(this, "filePath");
|
|
524
674
|
__publicField(this, "memoryCache", null);
|
|
675
|
+
__publicField(this, "credentialsCache", null);
|
|
525
676
|
__publicField(this, "initialized", false);
|
|
526
677
|
this.filePath = options.path || "./sessions.json";
|
|
527
678
|
}
|
|
@@ -536,14 +687,22 @@ var FileStorageBackend = class {
|
|
|
536
687
|
const data = await fs2.promises.readFile(this.filePath, "utf-8");
|
|
537
688
|
const json = JSON.parse(data);
|
|
538
689
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
690
|
+
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
691
|
+
if (Array.isArray(json.sessions)) {
|
|
692
|
+
json.sessions.forEach((s) => {
|
|
693
|
+
const session = normalizeStoredSession(s);
|
|
694
|
+
this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
if (Array.isArray(json.credentials)) {
|
|
698
|
+
json.credentials.forEach((c) => {
|
|
699
|
+
this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
|
|
542
700
|
});
|
|
543
701
|
}
|
|
544
702
|
} catch (error) {
|
|
545
703
|
if (error.code === "ENOENT") {
|
|
546
704
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
705
|
+
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
547
706
|
await this.flush();
|
|
548
707
|
} else {
|
|
549
708
|
console.error("[FileStorage] Failed to load sessions:", error);
|
|
@@ -557,9 +716,11 @@ var FileStorageBackend = class {
|
|
|
557
716
|
if (!this.initialized) await this.init();
|
|
558
717
|
}
|
|
559
718
|
async flush() {
|
|
560
|
-
if (!this.memoryCache) return;
|
|
561
|
-
|
|
562
|
-
|
|
719
|
+
if (!this.memoryCache || !this.credentialsCache) return;
|
|
720
|
+
await fs2.promises.writeFile(this.filePath, JSON.stringify({
|
|
721
|
+
sessions: Array.from(this.memoryCache.values()),
|
|
722
|
+
credentials: Array.from(this.credentialsCache.values())
|
|
723
|
+
}, null, 2), "utf-8");
|
|
563
724
|
}
|
|
564
725
|
getSessionKey(userId, sessionId) {
|
|
565
726
|
return `${userId}:${sessionId}`;
|
|
@@ -567,7 +728,7 @@ var FileStorageBackend = class {
|
|
|
567
728
|
generateSessionId() {
|
|
568
729
|
return generateSessionId();
|
|
569
730
|
}
|
|
570
|
-
async create(session
|
|
731
|
+
async create(session) {
|
|
571
732
|
await this.ensureInitialized();
|
|
572
733
|
const { sessionId, userId } = session;
|
|
573
734
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
@@ -575,10 +736,10 @@ var FileStorageBackend = class {
|
|
|
575
736
|
if (this.memoryCache.has(sessionKey)) {
|
|
576
737
|
throw new Error(`Session ${sessionId} already exists`);
|
|
577
738
|
}
|
|
578
|
-
this.memoryCache.set(sessionKey, session);
|
|
739
|
+
this.memoryCache.set(sessionKey, normalizeNewSession(session));
|
|
579
740
|
await this.flush();
|
|
580
741
|
}
|
|
581
|
-
async update(userId, sessionId, data
|
|
742
|
+
async update(userId, sessionId, data) {
|
|
582
743
|
await this.ensureInitialized();
|
|
583
744
|
if (!userId || !sessionId) throw new Error("userId and sessionId required");
|
|
584
745
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
@@ -586,18 +747,40 @@ var FileStorageBackend = class {
|
|
|
586
747
|
if (!current) {
|
|
587
748
|
throw new Error(`Session ${sessionId} not found`);
|
|
588
749
|
}
|
|
589
|
-
const updated =
|
|
590
|
-
...current,
|
|
591
|
-
...data
|
|
592
|
-
};
|
|
750
|
+
const updated = mergeSessionUpdate(current, data);
|
|
593
751
|
this.memoryCache.set(sessionKey, updated);
|
|
594
752
|
await this.flush();
|
|
595
753
|
}
|
|
754
|
+
async patchCredentials(userId, sessionId, data) {
|
|
755
|
+
await this.ensureInitialized();
|
|
756
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
757
|
+
if (!this.memoryCache.has(sessionKey)) {
|
|
758
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
759
|
+
}
|
|
760
|
+
const current = this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
|
|
761
|
+
this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
762
|
+
await this.flush();
|
|
763
|
+
}
|
|
596
764
|
async get(userId, sessionId) {
|
|
597
765
|
await this.ensureInitialized();
|
|
598
766
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
599
767
|
return this.memoryCache.get(sessionKey) || null;
|
|
600
768
|
}
|
|
769
|
+
async getCredentials(userId, sessionId) {
|
|
770
|
+
await this.ensureInitialized();
|
|
771
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
772
|
+
if (!this.memoryCache.has(sessionKey)) return null;
|
|
773
|
+
return this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
|
|
774
|
+
}
|
|
775
|
+
async clearCredentials(userId, sessionId) {
|
|
776
|
+
await this.patchCredentials(userId, sessionId, {
|
|
777
|
+
clientInformation: null,
|
|
778
|
+
tokens: null,
|
|
779
|
+
codeVerifier: null,
|
|
780
|
+
clientId: null,
|
|
781
|
+
oauthState: null
|
|
782
|
+
});
|
|
783
|
+
}
|
|
601
784
|
async list(userId) {
|
|
602
785
|
await this.ensureInitialized();
|
|
603
786
|
return Array.from(this.memoryCache.values()).filter((s) => s.userId === userId);
|
|
@@ -609,7 +792,9 @@ var FileStorageBackend = class {
|
|
|
609
792
|
async delete(userId, sessionId) {
|
|
610
793
|
await this.ensureInitialized();
|
|
611
794
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
612
|
-
|
|
795
|
+
const deleted = this.memoryCache.delete(sessionKey);
|
|
796
|
+
this.credentialsCache.delete(sessionKey);
|
|
797
|
+
if (deleted) {
|
|
613
798
|
await this.flush();
|
|
614
799
|
}
|
|
615
800
|
}
|
|
@@ -620,10 +805,21 @@ var FileStorageBackend = class {
|
|
|
620
805
|
async clearAll() {
|
|
621
806
|
await this.ensureInitialized();
|
|
622
807
|
this.memoryCache.clear();
|
|
808
|
+
this.credentialsCache.clear();
|
|
623
809
|
await this.flush();
|
|
624
810
|
}
|
|
625
811
|
async cleanupExpired() {
|
|
626
812
|
await this.ensureInitialized();
|
|
813
|
+
let changed = false;
|
|
814
|
+
for (const [key, session] of this.memoryCache.entries()) {
|
|
815
|
+
if (!isSessionExpired(session)) continue;
|
|
816
|
+
this.memoryCache.delete(key);
|
|
817
|
+
this.credentialsCache.delete(key);
|
|
818
|
+
changed = true;
|
|
819
|
+
}
|
|
820
|
+
if (changed) {
|
|
821
|
+
await this.flush();
|
|
822
|
+
}
|
|
627
823
|
}
|
|
628
824
|
async disconnect() {
|
|
629
825
|
}
|
|
@@ -635,10 +831,12 @@ var SqliteStorage = class {
|
|
|
635
831
|
constructor(options = {}) {
|
|
636
832
|
__publicField(this, "db", null);
|
|
637
833
|
__publicField(this, "table");
|
|
834
|
+
__publicField(this, "credentialsTable");
|
|
638
835
|
__publicField(this, "initialized", false);
|
|
639
836
|
__publicField(this, "dbPath");
|
|
640
837
|
this.dbPath = options.path || "./sessions.db";
|
|
641
838
|
this.table = options.table || "mcp_sessions";
|
|
839
|
+
this.credentialsTable = `${this.table}_credentials`;
|
|
642
840
|
}
|
|
643
841
|
async init() {
|
|
644
842
|
if (this.initialized) return;
|
|
@@ -649,6 +847,7 @@ var SqliteStorage = class {
|
|
|
649
847
|
fs2__namespace.mkdirSync(dir, { recursive: true });
|
|
650
848
|
}
|
|
651
849
|
this.db = new DatabaseConstructor(this.dbPath);
|
|
850
|
+
this.db.pragma("foreign_keys = ON");
|
|
652
851
|
this.db.exec(`
|
|
653
852
|
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
654
853
|
sessionId TEXT PRIMARY KEY,
|
|
@@ -657,6 +856,13 @@ var SqliteStorage = class {
|
|
|
657
856
|
expiresAt INTEGER
|
|
658
857
|
);
|
|
659
858
|
CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
|
|
859
|
+
CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
|
|
860
|
+
sessionId TEXT NOT NULL,
|
|
861
|
+
userId TEXT NOT NULL,
|
|
862
|
+
data TEXT NOT NULL,
|
|
863
|
+
PRIMARY KEY (userId, sessionId),
|
|
864
|
+
FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
|
|
865
|
+
);
|
|
660
866
|
`);
|
|
661
867
|
this.initialized = true;
|
|
662
868
|
console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
|
|
@@ -677,18 +883,18 @@ var SqliteStorage = class {
|
|
|
677
883
|
generateSessionId() {
|
|
678
884
|
return generateSessionId();
|
|
679
885
|
}
|
|
680
|
-
async create(session
|
|
886
|
+
async create(session) {
|
|
681
887
|
this.ensureInitialized();
|
|
682
888
|
const { sessionId, userId } = session;
|
|
683
889
|
if (!sessionId || !userId) {
|
|
684
890
|
throw new Error("userId and sessionId required");
|
|
685
891
|
}
|
|
686
|
-
const
|
|
892
|
+
const sessionWithLifecycle = normalizeNewSession(session);
|
|
687
893
|
try {
|
|
688
894
|
const stmt = this.db.prepare(
|
|
689
895
|
`INSERT INTO ${this.table} (sessionId, userId, data, expiresAt) VALUES (?, ?, ?, ?)`
|
|
690
896
|
);
|
|
691
|
-
stmt.run(sessionId, userId, JSON.stringify(
|
|
897
|
+
stmt.run(sessionId, userId, JSON.stringify(sessionWithLifecycle), sessionWithLifecycle.expiresAt ?? null);
|
|
692
898
|
} catch (error) {
|
|
693
899
|
if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
694
900
|
throw new Error(`Session ${sessionId} already exists`);
|
|
@@ -696,7 +902,7 @@ var SqliteStorage = class {
|
|
|
696
902
|
throw error;
|
|
697
903
|
}
|
|
698
904
|
}
|
|
699
|
-
async update(userId, sessionId, data
|
|
905
|
+
async update(userId, sessionId, data) {
|
|
700
906
|
this.ensureInitialized();
|
|
701
907
|
if (!sessionId || !userId) {
|
|
702
908
|
throw new Error("userId and sessionId required");
|
|
@@ -705,12 +911,25 @@ var SqliteStorage = class {
|
|
|
705
911
|
if (!currentSession) {
|
|
706
912
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
707
913
|
}
|
|
708
|
-
const updatedSession =
|
|
709
|
-
const expiresAt = ttl ? Date.now() + ttl * 1e3 : null;
|
|
914
|
+
const updatedSession = mergeSessionUpdate(currentSession, data);
|
|
710
915
|
const stmt = this.db.prepare(
|
|
711
916
|
`UPDATE ${this.table} SET data = ?, expiresAt = ? WHERE sessionId = ? AND userId = ?`
|
|
712
917
|
);
|
|
713
|
-
stmt.run(JSON.stringify(updatedSession), expiresAt, sessionId, userId);
|
|
918
|
+
stmt.run(JSON.stringify(updatedSession), updatedSession.expiresAt ?? null, sessionId, userId);
|
|
919
|
+
}
|
|
920
|
+
async patchCredentials(userId, sessionId, data) {
|
|
921
|
+
this.ensureInitialized();
|
|
922
|
+
if (!await this.get(userId, sessionId)) {
|
|
923
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
924
|
+
}
|
|
925
|
+
const current = await this.getCredentials(userId, sessionId) ?? { sessionId, userId };
|
|
926
|
+
const credentials = { ...current, ...data, sessionId, userId };
|
|
927
|
+
const stmt = this.db.prepare(
|
|
928
|
+
`INSERT INTO ${this.credentialsTable} (sessionId, userId, data)
|
|
929
|
+
VALUES (?, ?, ?)
|
|
930
|
+
ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
|
|
931
|
+
);
|
|
932
|
+
stmt.run(sessionId, userId, JSON.stringify(credentials));
|
|
714
933
|
}
|
|
715
934
|
async get(userId, sessionId) {
|
|
716
935
|
this.ensureInitialized();
|
|
@@ -719,7 +938,25 @@ var SqliteStorage = class {
|
|
|
719
938
|
);
|
|
720
939
|
const row = stmt.get(sessionId, userId);
|
|
721
940
|
if (!row) return null;
|
|
722
|
-
return JSON.parse(row.data);
|
|
941
|
+
return normalizeStoredSession(JSON.parse(row.data));
|
|
942
|
+
}
|
|
943
|
+
async getCredentials(userId, sessionId) {
|
|
944
|
+
this.ensureInitialized();
|
|
945
|
+
if (!await this.get(userId, sessionId)) return null;
|
|
946
|
+
const stmt = this.db.prepare(
|
|
947
|
+
`SELECT data FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
948
|
+
);
|
|
949
|
+
const row = stmt.get(sessionId, userId);
|
|
950
|
+
return row ? JSON.parse(row.data) : { sessionId, userId };
|
|
951
|
+
}
|
|
952
|
+
async clearCredentials(userId, sessionId) {
|
|
953
|
+
await this.patchCredentials(userId, sessionId, {
|
|
954
|
+
clientInformation: null,
|
|
955
|
+
tokens: null,
|
|
956
|
+
codeVerifier: null,
|
|
957
|
+
clientId: null,
|
|
958
|
+
oauthState: null
|
|
959
|
+
});
|
|
723
960
|
}
|
|
724
961
|
async list(userId) {
|
|
725
962
|
this.ensureInitialized();
|
|
@@ -727,7 +964,7 @@ var SqliteStorage = class {
|
|
|
727
964
|
`SELECT data FROM ${this.table} WHERE userId = ?`
|
|
728
965
|
);
|
|
729
966
|
const rows = stmt.all(userId);
|
|
730
|
-
return rows.map((row) => JSON.parse(row.data));
|
|
967
|
+
return rows.map((row) => normalizeStoredSession(JSON.parse(row.data)));
|
|
731
968
|
}
|
|
732
969
|
async listIds(userId) {
|
|
733
970
|
this.ensureInitialized();
|
|
@@ -742,6 +979,9 @@ var SqliteStorage = class {
|
|
|
742
979
|
const stmt = this.db.prepare(
|
|
743
980
|
`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
744
981
|
);
|
|
982
|
+
this.db.prepare(
|
|
983
|
+
`DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
984
|
+
).run(sessionId, userId);
|
|
745
985
|
stmt.run(sessionId, userId);
|
|
746
986
|
}
|
|
747
987
|
async listAllIds() {
|
|
@@ -752,16 +992,28 @@ var SqliteStorage = class {
|
|
|
752
992
|
}
|
|
753
993
|
async clearAll() {
|
|
754
994
|
this.ensureInitialized();
|
|
995
|
+
this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
|
|
755
996
|
const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
|
|
756
997
|
stmt.run();
|
|
757
998
|
}
|
|
758
999
|
async cleanupExpired() {
|
|
759
1000
|
this.ensureInitialized();
|
|
760
|
-
const
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1001
|
+
const rows = this.db.prepare(`SELECT sessionId, userId, data FROM ${this.table}`).all();
|
|
1002
|
+
const deleteStmt = this.db.prepare(`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`);
|
|
1003
|
+
for (const row of rows) {
|
|
1004
|
+
const session = normalizeStoredSession(JSON.parse(row.data));
|
|
1005
|
+
if (isSessionExpired(session)) {
|
|
1006
|
+
deleteStmt.run(row.sessionId, row.userId);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
this.db.prepare(
|
|
1010
|
+
`DELETE FROM ${this.credentialsTable}
|
|
1011
|
+
WHERE NOT EXISTS (
|
|
1012
|
+
SELECT 1 FROM ${this.table}
|
|
1013
|
+
WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
|
|
1014
|
+
AND ${this.table}.userId = ${this.credentialsTable}.userId
|
|
1015
|
+
)`
|
|
1016
|
+
).run();
|
|
765
1017
|
}
|
|
766
1018
|
async disconnect() {
|
|
767
1019
|
if (this.db) {
|
|
@@ -846,19 +1098,21 @@ function decryptObject(data) {
|
|
|
846
1098
|
var SupabaseStorageBackend = class {
|
|
847
1099
|
constructor(supabase) {
|
|
848
1100
|
this.supabase = supabase;
|
|
849
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
850
1101
|
}
|
|
851
1102
|
async init() {
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1103
|
+
await this.assertTable("mcp_sessions", "session_id");
|
|
1104
|
+
await this.assertTable("mcp_credentials", "session_id");
|
|
1105
|
+
console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
|
|
1106
|
+
}
|
|
1107
|
+
async assertTable(table, column) {
|
|
1108
|
+
const { error } = await this.supabase.from(table).select(column).limit(0);
|
|
1109
|
+
if (!error) return;
|
|
1110
|
+
if (error.code === "42P01") {
|
|
1111
|
+
throw new Error(
|
|
1112
|
+
`[SupabaseStorage] Table "${table}" not found in your database. Please run "npx mcp-ts supabase-init" to set up the required storage schema.`
|
|
1113
|
+
);
|
|
860
1114
|
}
|
|
861
|
-
|
|
1115
|
+
throw new Error(`[SupabaseStorage] Initialization check failed for "${table}": ${error.message}`);
|
|
862
1116
|
}
|
|
863
1117
|
generateSessionId() {
|
|
864
1118
|
return generateSessionId();
|
|
@@ -872,37 +1126,49 @@ var SupabaseStorageBackend = class {
|
|
|
872
1126
|
transportType: row.transport_type,
|
|
873
1127
|
callbackUrl: row.callback_url,
|
|
874
1128
|
createdAt: new Date(row.created_at).getTime(),
|
|
1129
|
+
updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
|
|
1130
|
+
expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
|
|
875
1131
|
userId: row.user_id,
|
|
876
1132
|
headers: decryptObject(row.headers),
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
tokens: decryptObject(row.tokens),
|
|
880
|
-
codeVerifier: row.code_verifier,
|
|
881
|
-
clientId: row.client_id
|
|
1133
|
+
authUrl: row.auth_url,
|
|
1134
|
+
status: row.status ?? "pending"
|
|
882
1135
|
};
|
|
883
1136
|
}
|
|
884
|
-
|
|
1137
|
+
mapRowToCredentials(row, userId, sessionId) {
|
|
1138
|
+
return {
|
|
1139
|
+
sessionId,
|
|
1140
|
+
userId,
|
|
1141
|
+
clientInformation: decryptObject(row?.client_information),
|
|
1142
|
+
tokens: decryptObject(row?.tokens),
|
|
1143
|
+
codeVerifier: decryptObject(row?.code_verifier),
|
|
1144
|
+
clientId: row?.client_id,
|
|
1145
|
+
oauthState: row?.oauth_state
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
hasCredentialData(data) {
|
|
1149
|
+
return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
|
|
1150
|
+
}
|
|
1151
|
+
async create(session) {
|
|
885
1152
|
const { sessionId, userId } = session;
|
|
886
1153
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
887
|
-
const
|
|
888
|
-
const
|
|
1154
|
+
const status = session.status ?? "pending";
|
|
1155
|
+
const createdAt = new Date(session.createdAt || Date.now()).toISOString();
|
|
1156
|
+
const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
|
|
1157
|
+
const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
|
|
889
1158
|
const { error } = await this.supabase.from("mcp_sessions").insert({
|
|
890
1159
|
session_id: sessionId,
|
|
891
1160
|
user_id: userId,
|
|
892
|
-
// Maps user_id to userId to support RLS using auth.uid()
|
|
893
1161
|
server_id: session.serverId,
|
|
894
1162
|
server_name: session.serverName,
|
|
895
1163
|
server_url: session.serverUrl,
|
|
896
1164
|
transport_type: session.transportType,
|
|
897
1165
|
callback_url: session.callbackUrl,
|
|
898
|
-
created_at:
|
|
1166
|
+
created_at: createdAt,
|
|
1167
|
+
updated_at: updatedAt,
|
|
899
1168
|
headers: encryptObject(session.headers),
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
code_verifier: session.codeVerifier,
|
|
904
|
-
client_id: session.clientId,
|
|
905
|
-
expires_at: expiresAt
|
|
1169
|
+
auth_url: session.authUrl ?? null,
|
|
1170
|
+
status,
|
|
1171
|
+
expires_at: expiresAt === null ? null : new Date(expiresAt).toISOString()
|
|
906
1172
|
});
|
|
907
1173
|
if (error) {
|
|
908
1174
|
if (error.code === "23505") {
|
|
@@ -911,11 +1177,8 @@ var SupabaseStorageBackend = class {
|
|
|
911
1177
|
throw new Error(`Failed to create session in Supabase: ${error.message}`);
|
|
912
1178
|
}
|
|
913
1179
|
}
|
|
914
|
-
async update(userId, sessionId, data
|
|
915
|
-
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
916
|
-
const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
|
|
1180
|
+
async update(userId, sessionId, data) {
|
|
917
1181
|
const updateData = {
|
|
918
|
-
expires_at: expiresAt,
|
|
919
1182
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
920
1183
|
};
|
|
921
1184
|
if ("serverId" in data) updateData.server_id = data.serverId;
|
|
@@ -923,20 +1186,50 @@ var SupabaseStorageBackend = class {
|
|
|
923
1186
|
if ("serverUrl" in data) updateData.server_url = data.serverUrl;
|
|
924
1187
|
if ("transportType" in data) updateData.transport_type = data.transportType;
|
|
925
1188
|
if ("callbackUrl" in data) updateData.callback_url = data.callbackUrl;
|
|
926
|
-
if ("
|
|
1189
|
+
if ("status" in data) {
|
|
1190
|
+
const status = data.status ?? "pending";
|
|
1191
|
+
const expiresAt = resolveSessionExpiresAt(status);
|
|
1192
|
+
updateData.status = status;
|
|
1193
|
+
updateData.expires_at = expiresAt === null ? null : new Date(expiresAt).toISOString();
|
|
1194
|
+
}
|
|
927
1195
|
if ("headers" in data) updateData.headers = encryptObject(data.headers);
|
|
928
|
-
if ("
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
if (
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1196
|
+
if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
|
|
1197
|
+
const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
|
|
1198
|
+
let updatedRows = null;
|
|
1199
|
+
if (shouldUpdateSession) {
|
|
1200
|
+
const result = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId).select("id");
|
|
1201
|
+
if (result.error) {
|
|
1202
|
+
throw new Error(`Failed to update session: ${result.error.message}`);
|
|
1203
|
+
}
|
|
1204
|
+
updatedRows = result.data;
|
|
1205
|
+
} else {
|
|
1206
|
+
const result = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
|
|
1207
|
+
if (result.error) {
|
|
1208
|
+
throw new Error(`Failed to update session: ${result.error.message}`);
|
|
1209
|
+
}
|
|
1210
|
+
updatedRows = result.data;
|
|
935
1211
|
}
|
|
936
1212
|
if (!updatedRows || updatedRows.length === 0) {
|
|
937
1213
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
938
1214
|
}
|
|
939
1215
|
}
|
|
1216
|
+
async patchCredentials(userId, sessionId, data) {
|
|
1217
|
+
if (!this.hasCredentialData(data)) return;
|
|
1218
|
+
const row = {
|
|
1219
|
+
user_id: userId,
|
|
1220
|
+
session_id: sessionId,
|
|
1221
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1222
|
+
};
|
|
1223
|
+
if ("clientInformation" in data) row.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
|
|
1224
|
+
if ("tokens" in data) row.tokens = data.tokens == null ? null : encryptObject(data.tokens);
|
|
1225
|
+
if ("codeVerifier" in data) row.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
|
|
1226
|
+
if ("clientId" in data) row.client_id = data.clientId ?? null;
|
|
1227
|
+
if ("oauthState" in data) row.oauth_state = data.oauthState ?? null;
|
|
1228
|
+
const { error } = await this.supabase.from("mcp_credentials").upsert(row, { onConflict: "user_id,session_id" });
|
|
1229
|
+
if (error) {
|
|
1230
|
+
throw new Error(`Failed to update credentials: ${error.message}`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
940
1233
|
async get(userId, sessionId) {
|
|
941
1234
|
const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
942
1235
|
if (error) {
|
|
@@ -946,6 +1239,21 @@ var SupabaseStorageBackend = class {
|
|
|
946
1239
|
if (!data) return null;
|
|
947
1240
|
return this.mapRowToSessionData(data);
|
|
948
1241
|
}
|
|
1242
|
+
async getCredentials(userId, sessionId) {
|
|
1243
|
+
const { data, error } = await this.supabase.from("mcp_credentials").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
1244
|
+
if (error) {
|
|
1245
|
+
console.error("[SupabaseStorage] Failed to get credentials:", error);
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
if (data) {
|
|
1249
|
+
return this.mapRowToCredentials(data, userId, sessionId);
|
|
1250
|
+
}
|
|
1251
|
+
const { data: sessionRows, error: sessionError } = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
|
|
1252
|
+
if (sessionError || !sessionRows || sessionRows.length === 0) {
|
|
1253
|
+
return null;
|
|
1254
|
+
}
|
|
1255
|
+
return { sessionId, userId };
|
|
1256
|
+
}
|
|
949
1257
|
async list(userId) {
|
|
950
1258
|
const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
|
|
951
1259
|
if (error) {
|
|
@@ -954,6 +1262,12 @@ var SupabaseStorageBackend = class {
|
|
|
954
1262
|
}
|
|
955
1263
|
return data.map((row) => this.mapRowToSessionData(row));
|
|
956
1264
|
}
|
|
1265
|
+
async clearCredentials(userId, sessionId) {
|
|
1266
|
+
const { error } = await this.supabase.from("mcp_credentials").delete().eq("user_id", userId).eq("session_id", sessionId);
|
|
1267
|
+
if (error) {
|
|
1268
|
+
throw new Error(`Failed to clear credentials: ${error.message}`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
957
1271
|
async delete(userId, sessionId) {
|
|
958
1272
|
const { error } = await this.supabase.from("mcp_sessions").delete().eq("user_id", userId).eq("session_id", sessionId);
|
|
959
1273
|
if (error) {
|
|
@@ -977,15 +1291,24 @@ var SupabaseStorageBackend = class {
|
|
|
977
1291
|
return data.map((row) => row.session_id);
|
|
978
1292
|
}
|
|
979
1293
|
async clearAll() {
|
|
1294
|
+
const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
|
|
1295
|
+
if (credentialsError) {
|
|
1296
|
+
console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
|
|
1297
|
+
}
|
|
980
1298
|
const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
|
|
981
1299
|
if (error) {
|
|
982
1300
|
console.error("[SupabaseStorage] Failed to clear sessions:", error);
|
|
983
1301
|
}
|
|
984
1302
|
}
|
|
985
1303
|
async cleanupExpired() {
|
|
986
|
-
const { error } = await this.supabase.from("mcp_sessions").delete().lt("expires_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
987
|
-
if (
|
|
988
|
-
console.error("[SupabaseStorage] Failed to cleanup expired sessions:",
|
|
1304
|
+
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());
|
|
1305
|
+
if (transientError) {
|
|
1306
|
+
console.error("[SupabaseStorage] Failed to cleanup expired inactive sessions:", transientError);
|
|
1307
|
+
}
|
|
1308
|
+
const dormantCutoff = new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString();
|
|
1309
|
+
const { error: dormantError } = await this.supabase.from("mcp_sessions").delete().eq("status", "active").lt("updated_at", dormantCutoff);
|
|
1310
|
+
if (dormantError) {
|
|
1311
|
+
console.error("[SupabaseStorage] Failed to cleanup dormant active sessions:", dormantError);
|
|
989
1312
|
}
|
|
990
1313
|
}
|
|
991
1314
|
async disconnect() {
|
|
@@ -997,23 +1320,29 @@ init_cjs_shims();
|
|
|
997
1320
|
var NeonStorageBackend = class {
|
|
998
1321
|
constructor(sql, options = {}) {
|
|
999
1322
|
this.sql = sql;
|
|
1000
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
1001
1323
|
__publicField(this, "tableName");
|
|
1324
|
+
__publicField(this, "credentialsTableName");
|
|
1002
1325
|
const schema = options.schema || "public";
|
|
1003
1326
|
const table = options.table || "mcp_sessions";
|
|
1327
|
+
const credentialsTable = options.credentialsTable || "mcp_credentials";
|
|
1004
1328
|
this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
|
|
1329
|
+
this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
|
|
1005
1330
|
}
|
|
1006
1331
|
async init() {
|
|
1332
|
+
await this.assertTable(this.tableName, "mcp_sessions");
|
|
1333
|
+
await this.assertTable(this.credentialsTableName, "mcp_credentials");
|
|
1334
|
+
console.log("[mcp-ts][Storage] Neon: storage tables verified.");
|
|
1335
|
+
}
|
|
1336
|
+
async assertTable(qualifiedName, displayName) {
|
|
1007
1337
|
const [{ exists } = { exists: null }] = await this.sql.query(
|
|
1008
1338
|
"SELECT to_regclass($1) AS exists",
|
|
1009
|
-
[
|
|
1339
|
+
[qualifiedName.replace(/"/g, "")]
|
|
1010
1340
|
);
|
|
1011
1341
|
if (!exists) {
|
|
1012
1342
|
throw new Error(
|
|
1013
|
-
|
|
1343
|
+
`[NeonStorage] Table "${displayName}" not found in your database. Please create it using the Neon storage guide in docs/storage-backends/neon.md.`
|
|
1014
1344
|
);
|
|
1015
1345
|
}
|
|
1016
|
-
console.log('[mcp-ts][Storage] Neon: "mcp_sessions" table verified.');
|
|
1017
1346
|
}
|
|
1018
1347
|
generateSessionId() {
|
|
1019
1348
|
return generateSessionId();
|
|
@@ -1033,20 +1362,35 @@ var NeonStorageBackend = class {
|
|
|
1033
1362
|
transportType: row.transport_type,
|
|
1034
1363
|
callbackUrl: row.callback_url,
|
|
1035
1364
|
createdAt: new Date(row.created_at).getTime(),
|
|
1365
|
+
updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
|
|
1366
|
+
expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
|
|
1036
1367
|
userId: row.user_id,
|
|
1037
1368
|
headers: decryptObject(row.headers),
|
|
1038
|
-
|
|
1039
|
-
|
|
1369
|
+
authUrl: row.auth_url ?? void 0,
|
|
1370
|
+
status: row.status ?? "pending"
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
mapRowToCredentials(row, userId, sessionId) {
|
|
1374
|
+
return {
|
|
1375
|
+
sessionId,
|
|
1376
|
+
userId,
|
|
1377
|
+
clientInformation: decryptObject(row.client_information),
|
|
1040
1378
|
tokens: decryptObject(row.tokens),
|
|
1041
|
-
codeVerifier: row.code_verifier
|
|
1042
|
-
clientId: row.client_id ?? void 0
|
|
1379
|
+
codeVerifier: decryptObject(row.code_verifier),
|
|
1380
|
+
clientId: row.client_id ?? void 0,
|
|
1381
|
+
oauthState: row.oauth_state
|
|
1043
1382
|
};
|
|
1044
1383
|
}
|
|
1045
|
-
|
|
1384
|
+
hasCredentialData(data) {
|
|
1385
|
+
return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
|
|
1386
|
+
}
|
|
1387
|
+
async create(session) {
|
|
1046
1388
|
const { sessionId, userId } = session;
|
|
1047
1389
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
1048
|
-
const
|
|
1049
|
-
const
|
|
1390
|
+
const status = session.status ?? "pending";
|
|
1391
|
+
const createdAt = new Date(session.createdAt || Date.now()).toISOString();
|
|
1392
|
+
const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
|
|
1393
|
+
const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
|
|
1050
1394
|
try {
|
|
1051
1395
|
await this.sql.query(
|
|
1052
1396
|
`INSERT INTO ${this.tableName} (
|
|
@@ -1058,16 +1402,14 @@ var NeonStorageBackend = class {
|
|
|
1058
1402
|
transport_type,
|
|
1059
1403
|
callback_url,
|
|
1060
1404
|
created_at,
|
|
1405
|
+
updated_at,
|
|
1061
1406
|
headers,
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
tokens,
|
|
1065
|
-
code_verifier,
|
|
1066
|
-
client_id,
|
|
1407
|
+
auth_url,
|
|
1408
|
+
status,
|
|
1067
1409
|
expires_at
|
|
1068
1410
|
) VALUES (
|
|
1069
1411
|
$1, $2, $3, $4, $5, $6, $7, $8,
|
|
1070
|
-
$9, $10, $11, $12, $13
|
|
1412
|
+
$9, $10, $11, $12, $13
|
|
1071
1413
|
)`,
|
|
1072
1414
|
[
|
|
1073
1415
|
sessionId,
|
|
@@ -1077,14 +1419,12 @@ var NeonStorageBackend = class {
|
|
|
1077
1419
|
session.serverUrl,
|
|
1078
1420
|
session.transportType,
|
|
1079
1421
|
session.callbackUrl,
|
|
1080
|
-
|
|
1422
|
+
createdAt,
|
|
1423
|
+
updatedAt,
|
|
1081
1424
|
encryptObject(session.headers),
|
|
1082
|
-
session.
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
session.codeVerifier,
|
|
1086
|
-
session.clientId,
|
|
1087
|
-
expiresAt
|
|
1425
|
+
session.authUrl ?? null,
|
|
1426
|
+
status,
|
|
1427
|
+
expiresAt === null ? null : new Date(expiresAt).toISOString()
|
|
1088
1428
|
]
|
|
1089
1429
|
);
|
|
1090
1430
|
} catch (error) {
|
|
@@ -1094,52 +1434,86 @@ var NeonStorageBackend = class {
|
|
|
1094
1434
|
throw new Error(`Failed to create session in Neon: ${error.message}`);
|
|
1095
1435
|
}
|
|
1096
1436
|
}
|
|
1097
|
-
async update(userId, sessionId, data
|
|
1437
|
+
async update(userId, sessionId, data) {
|
|
1098
1438
|
const currentSession = await this.get(userId, sessionId);
|
|
1099
1439
|
if (!currentSession) {
|
|
1100
1440
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1101
1441
|
}
|
|
1102
1442
|
const updatedSession = { ...currentSession, ...data };
|
|
1103
|
-
const
|
|
1104
|
-
const expiresAt =
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1443
|
+
const status = updatedSession.status ?? "pending";
|
|
1444
|
+
const expiresAt = resolveSessionExpiresAt(status);
|
|
1445
|
+
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;
|
|
1446
|
+
if (shouldUpdateSession) {
|
|
1447
|
+
const updatedRows = await this.sql.query(
|
|
1448
|
+
`UPDATE ${this.tableName}
|
|
1449
|
+
SET
|
|
1450
|
+
server_id = $1,
|
|
1451
|
+
server_name = $2,
|
|
1452
|
+
server_url = $3,
|
|
1453
|
+
transport_type = $4,
|
|
1454
|
+
callback_url = $5,
|
|
1455
|
+
status = $6,
|
|
1456
|
+
headers = $7,
|
|
1457
|
+
auth_url = $8,
|
|
1458
|
+
expires_at = $9,
|
|
1459
|
+
updated_at = now()
|
|
1460
|
+
WHERE user_id = $10 AND session_id = $11
|
|
1461
|
+
RETURNING id`,
|
|
1462
|
+
[
|
|
1463
|
+
updatedSession.serverId,
|
|
1464
|
+
updatedSession.serverName,
|
|
1465
|
+
updatedSession.serverUrl,
|
|
1466
|
+
updatedSession.transportType,
|
|
1467
|
+
updatedSession.callbackUrl,
|
|
1468
|
+
status,
|
|
1469
|
+
encryptObject(updatedSession.headers),
|
|
1470
|
+
updatedSession.authUrl ?? null,
|
|
1471
|
+
expiresAt === null ? null : new Date(expiresAt).toISOString(),
|
|
1472
|
+
userId,
|
|
1473
|
+
sessionId
|
|
1474
|
+
]
|
|
1475
|
+
);
|
|
1476
|
+
if (updatedRows.length === 0) {
|
|
1477
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
async patchCredentials(userId, sessionId, data) {
|
|
1482
|
+
if (!this.hasCredentialData(data)) return;
|
|
1483
|
+
await this.sql.query(
|
|
1484
|
+
`INSERT INTO ${this.credentialsTableName} (
|
|
1485
|
+
user_id,
|
|
1486
|
+
session_id,
|
|
1487
|
+
client_information,
|
|
1488
|
+
tokens,
|
|
1489
|
+
code_verifier,
|
|
1490
|
+
client_id,
|
|
1491
|
+
oauth_state,
|
|
1492
|
+
updated_at
|
|
1493
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, now())
|
|
1494
|
+
ON CONFLICT (user_id, session_id)
|
|
1495
|
+
DO UPDATE SET
|
|
1496
|
+
client_information = CASE WHEN $8 THEN EXCLUDED.client_information ELSE ${this.credentialsTableName}.client_information END,
|
|
1497
|
+
tokens = CASE WHEN $9 THEN EXCLUDED.tokens ELSE ${this.credentialsTableName}.tokens END,
|
|
1498
|
+
code_verifier = CASE WHEN $10 THEN EXCLUDED.code_verifier ELSE ${this.credentialsTableName}.code_verifier END,
|
|
1499
|
+
client_id = CASE WHEN $11 THEN EXCLUDED.client_id ELSE ${this.credentialsTableName}.client_id END,
|
|
1500
|
+
oauth_state = CASE WHEN $12 THEN EXCLUDED.oauth_state ELSE ${this.credentialsTableName}.oauth_state END,
|
|
1501
|
+
updated_at = now()`,
|
|
1123
1502
|
[
|
|
1124
|
-
updatedSession.serverId,
|
|
1125
|
-
updatedSession.serverName,
|
|
1126
|
-
updatedSession.serverUrl,
|
|
1127
|
-
updatedSession.transportType,
|
|
1128
|
-
updatedSession.callbackUrl,
|
|
1129
|
-
updatedSession.active ?? false,
|
|
1130
|
-
encryptObject(updatedSession.headers),
|
|
1131
|
-
updatedSession.clientInformation,
|
|
1132
|
-
updatedSession.tokens === void 0 ? null : encryptObject(updatedSession.tokens),
|
|
1133
|
-
updatedSession.codeVerifier,
|
|
1134
|
-
updatedSession.clientId,
|
|
1135
|
-
expiresAt,
|
|
1136
1503
|
userId,
|
|
1137
|
-
sessionId
|
|
1504
|
+
sessionId,
|
|
1505
|
+
"clientInformation" in data ? data.clientInformation == null ? null : encryptObject(data.clientInformation) : null,
|
|
1506
|
+
"tokens" in data ? data.tokens == null ? null : encryptObject(data.tokens) : null,
|
|
1507
|
+
"codeVerifier" in data ? data.codeVerifier == null ? null : encryptObject(data.codeVerifier) : null,
|
|
1508
|
+
"clientId" in data ? data.clientId ?? null : null,
|
|
1509
|
+
"oauthState" in data ? data.oauthState ?? null : null,
|
|
1510
|
+
"clientInformation" in data,
|
|
1511
|
+
"tokens" in data,
|
|
1512
|
+
"codeVerifier" in data,
|
|
1513
|
+
"clientId" in data,
|
|
1514
|
+
"oauthState" in data
|
|
1138
1515
|
]
|
|
1139
1516
|
);
|
|
1140
|
-
if (updatedRows.length === 0) {
|
|
1141
|
-
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1142
|
-
}
|
|
1143
1517
|
}
|
|
1144
1518
|
async get(userId, sessionId) {
|
|
1145
1519
|
try {
|
|
@@ -1147,12 +1521,32 @@ var NeonStorageBackend = class {
|
|
|
1147
1521
|
`SELECT * FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1148
1522
|
[userId, sessionId]
|
|
1149
1523
|
);
|
|
1150
|
-
|
|
1524
|
+
if (!rows[0]) return null;
|
|
1525
|
+
return this.mapRowToSessionData(rows[0]);
|
|
1151
1526
|
} catch (error) {
|
|
1152
1527
|
console.error("[NeonStorage] Failed to get session:", error);
|
|
1153
1528
|
return null;
|
|
1154
1529
|
}
|
|
1155
1530
|
}
|
|
1531
|
+
async getCredentials(userId, sessionId) {
|
|
1532
|
+
try {
|
|
1533
|
+
const credentialRows = await this.sql.query(
|
|
1534
|
+
`SELECT * FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1535
|
+
[userId, sessionId]
|
|
1536
|
+
);
|
|
1537
|
+
if (credentialRows[0]) {
|
|
1538
|
+
return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
|
|
1539
|
+
}
|
|
1540
|
+
const sessionRows = await this.sql.query(
|
|
1541
|
+
`SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1542
|
+
[userId, sessionId]
|
|
1543
|
+
);
|
|
1544
|
+
return sessionRows[0] ? { sessionId, userId } : null;
|
|
1545
|
+
} catch (error) {
|
|
1546
|
+
console.error("[NeonStorage] Failed to get credentials:", error);
|
|
1547
|
+
return null;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1156
1550
|
async list(userId) {
|
|
1157
1551
|
try {
|
|
1158
1552
|
const rows = await this.sql.query(
|
|
@@ -1165,6 +1559,16 @@ var NeonStorageBackend = class {
|
|
|
1165
1559
|
return [];
|
|
1166
1560
|
}
|
|
1167
1561
|
}
|
|
1562
|
+
async clearCredentials(userId, sessionId) {
|
|
1563
|
+
try {
|
|
1564
|
+
await this.sql.query(
|
|
1565
|
+
`DELETE FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1566
|
+
[userId, sessionId]
|
|
1567
|
+
);
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
console.error("[NeonStorage] Failed to clear credentials:", error);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1168
1572
|
async delete(userId, sessionId) {
|
|
1169
1573
|
try {
|
|
1170
1574
|
await this.sql.query(
|
|
@@ -1200,6 +1604,7 @@ var NeonStorageBackend = class {
|
|
|
1200
1604
|
}
|
|
1201
1605
|
async clearAll() {
|
|
1202
1606
|
try {
|
|
1607
|
+
await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
|
|
1203
1608
|
await this.sql.query(`DELETE FROM ${this.tableName}`);
|
|
1204
1609
|
} catch (error) {
|
|
1205
1610
|
console.error("[NeonStorage] Failed to clear sessions:", error);
|
|
@@ -1208,9 +1613,17 @@ var NeonStorageBackend = class {
|
|
|
1208
1613
|
async cleanupExpired() {
|
|
1209
1614
|
try {
|
|
1210
1615
|
await this.sql.query(
|
|
1211
|
-
`DELETE FROM ${this.tableName}
|
|
1616
|
+
`DELETE FROM ${this.tableName}
|
|
1617
|
+
WHERE expires_at IS NOT NULL
|
|
1618
|
+
AND expires_at < $1
|
|
1619
|
+
AND status <> 'active'`,
|
|
1212
1620
|
[(/* @__PURE__ */ new Date()).toISOString()]
|
|
1213
1621
|
);
|
|
1622
|
+
await this.sql.query(
|
|
1623
|
+
`DELETE FROM ${this.tableName}
|
|
1624
|
+
WHERE status = 'active' AND updated_at < $1`,
|
|
1625
|
+
[new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString()]
|
|
1626
|
+
);
|
|
1214
1627
|
} catch (error) {
|
|
1215
1628
|
console.error("[NeonStorage] Failed to cleanup expired sessions:", error);
|
|
1216
1629
|
}
|
|
@@ -1260,26 +1673,24 @@ function emitSessionMutation(event) {
|
|
|
1260
1673
|
function createSessionMutationEvent(prop, args) {
|
|
1261
1674
|
const timestamp = Date.now();
|
|
1262
1675
|
if (prop === "create") {
|
|
1263
|
-
const [session
|
|
1676
|
+
const [session] = args;
|
|
1264
1677
|
if (!session?.userId || !session?.sessionId) return null;
|
|
1265
1678
|
return {
|
|
1266
1679
|
type: "create",
|
|
1267
1680
|
userId: session.userId,
|
|
1268
1681
|
sessionId: session.sessionId,
|
|
1269
1682
|
session,
|
|
1270
|
-
ttl,
|
|
1271
1683
|
timestamp
|
|
1272
1684
|
};
|
|
1273
1685
|
}
|
|
1274
1686
|
if (prop === "update") {
|
|
1275
|
-
const [userId, sessionId, patch
|
|
1687
|
+
const [userId, sessionId, patch] = args;
|
|
1276
1688
|
if (!userId || !sessionId) return null;
|
|
1277
1689
|
return {
|
|
1278
1690
|
type: "update",
|
|
1279
1691
|
userId,
|
|
1280
1692
|
sessionId,
|
|
1281
1693
|
patch,
|
|
1282
|
-
ttl,
|
|
1283
1694
|
timestamp
|
|
1284
1695
|
};
|
|
1285
1696
|
}
|
|
@@ -1475,7 +1886,6 @@ var StorageOAuthClientProvider = class {
|
|
|
1475
1886
|
__publicField(this, "_authUrl");
|
|
1476
1887
|
__publicField(this, "_clientId");
|
|
1477
1888
|
__publicField(this, "onRedirectCallback");
|
|
1478
|
-
__publicField(this, "tokenExpiresAt");
|
|
1479
1889
|
this.userId = options.userId;
|
|
1480
1890
|
this.serverId = options.serverId;
|
|
1481
1891
|
this.sessionId = options.sessionId;
|
|
@@ -1509,30 +1919,30 @@ var StorageOAuthClientProvider = class {
|
|
|
1509
1919
|
this._clientId = clientId_;
|
|
1510
1920
|
}
|
|
1511
1921
|
/**
|
|
1512
|
-
* Loads OAuth
|
|
1922
|
+
* Loads OAuth credentials from the session store
|
|
1513
1923
|
* @private
|
|
1514
1924
|
*/
|
|
1515
|
-
async
|
|
1516
|
-
const data = await sessions.
|
|
1925
|
+
async getCredentials() {
|
|
1926
|
+
const data = await sessions.getCredentials(this.userId, this.sessionId);
|
|
1517
1927
|
if (!data) {
|
|
1518
|
-
return {};
|
|
1928
|
+
return { userId: this.userId, sessionId: this.sessionId };
|
|
1519
1929
|
}
|
|
1520
1930
|
return data;
|
|
1521
1931
|
}
|
|
1522
1932
|
/**
|
|
1523
|
-
* Saves OAuth
|
|
1524
|
-
* @param data - Partial OAuth
|
|
1933
|
+
* Saves OAuth credentials to the session store
|
|
1934
|
+
* @param data - Partial OAuth credentials to save
|
|
1525
1935
|
* @private
|
|
1526
1936
|
* @throws Error if session doesn't exist (session must be created by controller layer)
|
|
1527
1937
|
*/
|
|
1528
|
-
async
|
|
1529
|
-
await sessions.
|
|
1938
|
+
async patchCredentials(data) {
|
|
1939
|
+
await sessions.patchCredentials(this.userId, this.sessionId, data);
|
|
1530
1940
|
}
|
|
1531
1941
|
/**
|
|
1532
1942
|
* Retrieves stored OAuth client information
|
|
1533
1943
|
*/
|
|
1534
1944
|
async clientInformation() {
|
|
1535
|
-
const data = await this.
|
|
1945
|
+
const data = await this.getCredentials();
|
|
1536
1946
|
if (data.clientId && !this._clientId) {
|
|
1537
1947
|
this._clientId = data.clientId;
|
|
1538
1948
|
}
|
|
@@ -1551,7 +1961,7 @@ var StorageOAuthClientProvider = class {
|
|
|
1551
1961
|
* Stores OAuth client information
|
|
1552
1962
|
*/
|
|
1553
1963
|
async saveClientInformation(clientInformation) {
|
|
1554
|
-
await this.
|
|
1964
|
+
await this.patchCredentials({
|
|
1555
1965
|
clientInformation,
|
|
1556
1966
|
clientId: clientInformation.client_id
|
|
1557
1967
|
});
|
|
@@ -1561,29 +1971,58 @@ var StorageOAuthClientProvider = class {
|
|
|
1561
1971
|
* Stores OAuth tokens
|
|
1562
1972
|
*/
|
|
1563
1973
|
async saveTokens(tokens) {
|
|
1564
|
-
|
|
1565
|
-
if (tokens.expires_in) {
|
|
1566
|
-
this.tokenExpiresAt = Date.now() + tokens.expires_in * 1e3 - TOKEN_EXPIRY_BUFFER_MS;
|
|
1567
|
-
}
|
|
1568
|
-
await this.saveSessionData(data);
|
|
1974
|
+
await this.patchCredentials({ tokens });
|
|
1569
1975
|
}
|
|
1570
1976
|
get authUrl() {
|
|
1571
1977
|
return this._authUrl;
|
|
1572
1978
|
}
|
|
1573
1979
|
async state() {
|
|
1574
|
-
|
|
1980
|
+
const nonce = nanoid.nanoid(32);
|
|
1981
|
+
await this.patchCredentials({
|
|
1982
|
+
oauthState: {
|
|
1983
|
+
nonce,
|
|
1984
|
+
sessionId: this.sessionId,
|
|
1985
|
+
serverId: this.serverId,
|
|
1986
|
+
createdAt: Date.now()
|
|
1987
|
+
},
|
|
1988
|
+
codeVerifier: null
|
|
1989
|
+
});
|
|
1990
|
+
return formatOAuthState(nonce, this.sessionId);
|
|
1575
1991
|
}
|
|
1576
|
-
async checkState(
|
|
1577
|
-
const
|
|
1992
|
+
async checkState(state) {
|
|
1993
|
+
const parsed = parseOAuthState(state);
|
|
1994
|
+
if (!parsed) {
|
|
1995
|
+
return { valid: false, error: "Invalid OAuth state" };
|
|
1996
|
+
}
|
|
1997
|
+
if (parsed.sessionId !== this.sessionId) {
|
|
1998
|
+
return { valid: false, error: "OAuth state mismatch" };
|
|
1999
|
+
}
|
|
2000
|
+
const data = await sessions.getCredentials(this.userId, parsed.sessionId);
|
|
1578
2001
|
if (!data) {
|
|
1579
2002
|
return { valid: false, error: "Session not found" };
|
|
1580
2003
|
}
|
|
1581
|
-
|
|
2004
|
+
const oauthState = data.oauthState;
|
|
2005
|
+
if (!oauthState) {
|
|
2006
|
+
return { valid: false, error: "OAuth state not found" };
|
|
2007
|
+
}
|
|
2008
|
+
if (oauthState.nonce !== parsed.nonce || oauthState.sessionId !== parsed.sessionId || oauthState.serverId !== this.serverId) {
|
|
2009
|
+
return { valid: false, error: "OAuth state mismatch" };
|
|
2010
|
+
}
|
|
2011
|
+
if (Date.now() - oauthState.createdAt > STATE_EXPIRATION_MS) {
|
|
2012
|
+
return { valid: false, error: "OAuth state expired" };
|
|
2013
|
+
}
|
|
2014
|
+
return { valid: true, serverId: oauthState.serverId };
|
|
1582
2015
|
}
|
|
1583
|
-
async consumeState(
|
|
2016
|
+
async consumeState(state) {
|
|
2017
|
+
const result = await this.checkState(state);
|
|
2018
|
+
if (!result.valid) {
|
|
2019
|
+
throw new Error(result.error || "Invalid OAuth state");
|
|
2020
|
+
}
|
|
2021
|
+
await this.patchCredentials({ oauthState: null });
|
|
1584
2022
|
}
|
|
1585
2023
|
async redirectToAuthorization(authUrl) {
|
|
1586
2024
|
this._authUrl = authUrl.toString();
|
|
2025
|
+
await sessions.update(this.userId, this.sessionId, { authUrl: this._authUrl });
|
|
1587
2026
|
if (this.onRedirectCallback) {
|
|
1588
2027
|
this.onRedirectCallback(authUrl.toString());
|
|
1589
2028
|
}
|
|
@@ -1594,21 +2033,25 @@ var StorageOAuthClientProvider = class {
|
|
|
1594
2033
|
} else {
|
|
1595
2034
|
const updates = {};
|
|
1596
2035
|
if (scope === "client") {
|
|
1597
|
-
updates.clientInformation =
|
|
1598
|
-
updates.clientId =
|
|
2036
|
+
updates.clientInformation = null;
|
|
2037
|
+
updates.clientId = null;
|
|
1599
2038
|
} else if (scope === "tokens") {
|
|
1600
|
-
updates.tokens =
|
|
2039
|
+
updates.tokens = null;
|
|
1601
2040
|
} else if (scope === "verifier") {
|
|
1602
|
-
updates.codeVerifier =
|
|
2041
|
+
updates.codeVerifier = null;
|
|
1603
2042
|
}
|
|
1604
|
-
await this.
|
|
2043
|
+
await this.patchCredentials(updates);
|
|
1605
2044
|
}
|
|
1606
2045
|
}
|
|
1607
2046
|
async saveCodeVerifier(verifier) {
|
|
1608
|
-
await this.
|
|
2047
|
+
const data = await this.getCredentials();
|
|
2048
|
+
if (data.codeVerifier) {
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
await this.patchCredentials({ codeVerifier: verifier });
|
|
1609
2052
|
}
|
|
1610
2053
|
async codeVerifier() {
|
|
1611
|
-
const data = await this.
|
|
2054
|
+
const data = await this.getCredentials();
|
|
1612
2055
|
if (data.clientId && !this._clientId) {
|
|
1613
2056
|
this._clientId = data.clientId;
|
|
1614
2057
|
}
|
|
@@ -1618,23 +2061,14 @@ var StorageOAuthClientProvider = class {
|
|
|
1618
2061
|
return data.codeVerifier;
|
|
1619
2062
|
}
|
|
1620
2063
|
async deleteCodeVerifier() {
|
|
1621
|
-
await this.
|
|
2064
|
+
await this.patchCredentials({ codeVerifier: null });
|
|
1622
2065
|
}
|
|
1623
2066
|
async tokens() {
|
|
1624
|
-
const data = await this.
|
|
2067
|
+
const data = await this.getCredentials();
|
|
1625
2068
|
if (data.clientId && !this._clientId) {
|
|
1626
2069
|
this._clientId = data.clientId;
|
|
1627
2070
|
}
|
|
1628
|
-
return data.tokens;
|
|
1629
|
-
}
|
|
1630
|
-
isTokenExpired() {
|
|
1631
|
-
if (!this.tokenExpiresAt) {
|
|
1632
|
-
return false;
|
|
1633
|
-
}
|
|
1634
|
-
return Date.now() >= this.tokenExpiresAt;
|
|
1635
|
-
}
|
|
1636
|
-
setTokenExpiresAt(expiresAt) {
|
|
1637
|
-
this.tokenExpiresAt = expiresAt;
|
|
2071
|
+
return data.tokens ?? void 0;
|
|
1638
2072
|
}
|
|
1639
2073
|
};
|
|
1640
2074
|
|
|
@@ -1929,7 +2363,7 @@ var MCPClient = class {
|
|
|
1929
2363
|
* Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
|
|
1930
2364
|
* This wrapper enforces a timeout and properly uses AbortController to unblock the request.
|
|
1931
2365
|
*/
|
|
1932
|
-
fetch: (url, init) => {
|
|
2366
|
+
fetch: async (url, init) => {
|
|
1933
2367
|
const timeout = 3e4;
|
|
1934
2368
|
const controller = new AbortController();
|
|
1935
2369
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
@@ -1937,7 +2371,17 @@ var MCPClient = class {
|
|
|
1937
2371
|
// @ts-ignore: AbortSignal.any is available in Node 20+
|
|
1938
2372
|
AbortSignal.any ? AbortSignal.any([init.signal, controller.signal]) : controller.signal
|
|
1939
2373
|
) : controller.signal;
|
|
1940
|
-
|
|
2374
|
+
try {
|
|
2375
|
+
const response = await fetch(url, { ...init, signal });
|
|
2376
|
+
const hasSessionHeader = init?.headers && new Headers(init.headers).has("mcp-session-id");
|
|
2377
|
+
if (response.status === 404 && hasSessionHeader) {
|
|
2378
|
+
this.client = null;
|
|
2379
|
+
throw new Error("MCP_SESSION_EXPIRED: Downstream session was not found on the server.");
|
|
2380
|
+
}
|
|
2381
|
+
return response;
|
|
2382
|
+
} finally {
|
|
2383
|
+
clearTimeout(timeoutId);
|
|
2384
|
+
}
|
|
1941
2385
|
}
|
|
1942
2386
|
};
|
|
1943
2387
|
if (type === "sse") {
|
|
@@ -1958,17 +2402,18 @@ var MCPClient = class {
|
|
|
1958
2402
|
}
|
|
1959
2403
|
this.emitStateChange("INITIALIZING");
|
|
1960
2404
|
this.emitProgress("Loading session configuration...");
|
|
2405
|
+
let existingSession = null;
|
|
1961
2406
|
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
1962
|
-
|
|
1963
|
-
if (!
|
|
2407
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2408
|
+
if (!existingSession) {
|
|
1964
2409
|
throw new Error(`Session not found: ${this.sessionId}`);
|
|
1965
2410
|
}
|
|
1966
|
-
this.serverUrl = this.serverUrl ||
|
|
1967
|
-
this.callbackUrl = this.callbackUrl ||
|
|
1968
|
-
this.serverName = this.serverName ||
|
|
1969
|
-
this.serverId = this.serverId ||
|
|
1970
|
-
this.headers = this.headers ||
|
|
1971
|
-
this.createdAt =
|
|
2411
|
+
this.serverUrl = this.serverUrl || existingSession.serverUrl;
|
|
2412
|
+
this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
|
|
2413
|
+
this.serverName = this.serverName || existingSession.serverName;
|
|
2414
|
+
this.serverId = this.serverId || existingSession.serverId || "unknown";
|
|
2415
|
+
this.headers = this.headers || existingSession.headers;
|
|
2416
|
+
this.createdAt = existingSession.createdAt;
|
|
1972
2417
|
}
|
|
1973
2418
|
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
1974
2419
|
throw new Error("Missing required connection metadata");
|
|
@@ -2012,10 +2457,13 @@ var MCPClient = class {
|
|
|
2012
2457
|
}
|
|
2013
2458
|
);
|
|
2014
2459
|
}
|
|
2015
|
-
|
|
2460
|
+
if (existingSession === null) {
|
|
2461
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2462
|
+
}
|
|
2016
2463
|
if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
|
|
2017
2464
|
this.createdAt = Date.now();
|
|
2018
|
-
|
|
2465
|
+
const updatedAt = this.createdAt;
|
|
2466
|
+
console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
|
|
2019
2467
|
await sessions.create({
|
|
2020
2468
|
sessionId: this.sessionId,
|
|
2021
2469
|
userId: this.userId,
|
|
@@ -2026,18 +2474,18 @@ var MCPClient = class {
|
|
|
2026
2474
|
transportType: this.transportType || "streamable-http",
|
|
2027
2475
|
headers: this.headers,
|
|
2028
2476
|
createdAt: this.createdAt,
|
|
2029
|
-
|
|
2030
|
-
|
|
2477
|
+
updatedAt,
|
|
2478
|
+
status: "pending"
|
|
2479
|
+
});
|
|
2031
2480
|
}
|
|
2032
2481
|
}
|
|
2033
2482
|
/**
|
|
2034
2483
|
* Saves current session state to the session store
|
|
2035
2484
|
* Creates new session if it doesn't exist, updates if it does
|
|
2036
|
-
* @param
|
|
2037
|
-
* @param active - Session status marker used to avoid unnecessary TTL rewrites
|
|
2485
|
+
* @param status - Session lifecycle status used by storage cleanup
|
|
2038
2486
|
* @private
|
|
2039
2487
|
*/
|
|
2040
|
-
async saveSession(
|
|
2488
|
+
async saveSession(status = "active", existingSession) {
|
|
2041
2489
|
if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
|
|
2042
2490
|
return;
|
|
2043
2491
|
}
|
|
@@ -2051,13 +2499,29 @@ var MCPClient = class {
|
|
|
2051
2499
|
transportType: this.transportType || "streamable-http",
|
|
2052
2500
|
headers: this.headers,
|
|
2053
2501
|
createdAt: this.createdAt || Date.now(),
|
|
2054
|
-
|
|
2502
|
+
updatedAt: Date.now(),
|
|
2503
|
+
status
|
|
2055
2504
|
};
|
|
2056
|
-
|
|
2505
|
+
if (status === "active") {
|
|
2506
|
+
sessionData.authUrl = null;
|
|
2507
|
+
}
|
|
2508
|
+
if (existingSession === void 0) {
|
|
2509
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2510
|
+
}
|
|
2057
2511
|
if (existingSession) {
|
|
2058
|
-
await sessions.update(this.userId, this.sessionId, sessionData
|
|
2512
|
+
await sessions.update(this.userId, this.sessionId, sessionData);
|
|
2059
2513
|
} else {
|
|
2060
|
-
await sessions.create(sessionData
|
|
2514
|
+
await sessions.create(sessionData);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
/**
|
|
2518
|
+
* Removes transient setup/auth sessions without masking the original error.
|
|
2519
|
+
* @private
|
|
2520
|
+
*/
|
|
2521
|
+
async deleteTransientSession() {
|
|
2522
|
+
try {
|
|
2523
|
+
await sessions.delete(this.userId, this.sessionId);
|
|
2524
|
+
} catch {
|
|
2061
2525
|
}
|
|
2062
2526
|
}
|
|
2063
2527
|
/**
|
|
@@ -2109,6 +2573,14 @@ var MCPClient = class {
|
|
|
2109
2573
|
* @throws {Error} When connection fails for other reasons
|
|
2110
2574
|
*/
|
|
2111
2575
|
async connect() {
|
|
2576
|
+
if (this.client?.transport) {
|
|
2577
|
+
this.transport = null;
|
|
2578
|
+
try {
|
|
2579
|
+
await this.client.close();
|
|
2580
|
+
} catch {
|
|
2581
|
+
}
|
|
2582
|
+
this.client = null;
|
|
2583
|
+
}
|
|
2112
2584
|
await this.initialize();
|
|
2113
2585
|
if (!this.client || !this.oauthProvider) {
|
|
2114
2586
|
const error = "Client or OAuth provider not initialized";
|
|
@@ -2122,8 +2594,8 @@ var MCPClient = class {
|
|
|
2122
2594
|
this.transportType = transportType;
|
|
2123
2595
|
this.emitStateChange("CONNECTED");
|
|
2124
2596
|
this.emitProgress("Connected successfully");
|
|
2125
|
-
console.log(`[MCPClient] Saving session ${this.sessionId}
|
|
2126
|
-
await this.saveSession(
|
|
2597
|
+
console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
|
|
2598
|
+
await this.saveSession("active");
|
|
2127
2599
|
} catch (error) {
|
|
2128
2600
|
if (error instanceof auth_js.UnauthorizedError || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
|
|
2129
2601
|
let authUrl = "";
|
|
@@ -2135,15 +2607,12 @@ var MCPClient = class {
|
|
|
2135
2607
|
const message = detail.toLowerCase() === "unauthorized" ? "OAuth authorization URL not available" : `OAuth authorization URL not available: ${detail}`;
|
|
2136
2608
|
this.emitError(message, "auth");
|
|
2137
2609
|
this.emitStateChange("FAILED");
|
|
2138
|
-
|
|
2139
|
-
await sessions.delete(this.userId, this.sessionId);
|
|
2140
|
-
} catch {
|
|
2141
|
-
}
|
|
2610
|
+
await this.deleteTransientSession();
|
|
2142
2611
|
throw new Error(message);
|
|
2143
2612
|
}
|
|
2144
2613
|
this.emitStateChange("AUTHENTICATING");
|
|
2145
|
-
console.log(`[MCPClient] Saving session ${this.sessionId}
|
|
2146
|
-
await this.saveSession(
|
|
2614
|
+
console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
|
|
2615
|
+
await this.saveSession("pending");
|
|
2147
2616
|
if (this.serverId) {
|
|
2148
2617
|
this._onConnectionEvent.fire({
|
|
2149
2618
|
type: "auth_required",
|
|
@@ -2163,7 +2632,7 @@ var MCPClient = class {
|
|
|
2163
2632
|
this.emitStateChange("FAILED");
|
|
2164
2633
|
try {
|
|
2165
2634
|
const existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2166
|
-
if (!existingSession || existingSession.
|
|
2635
|
+
if (!existingSession || existingSession.status !== "active") {
|
|
2167
2636
|
await sessions.delete(this.userId, this.sessionId);
|
|
2168
2637
|
}
|
|
2169
2638
|
} catch {
|
|
@@ -2178,7 +2647,7 @@ var MCPClient = class {
|
|
|
2178
2647
|
* @param authCode - Authorization code received from OAuth callback
|
|
2179
2648
|
*/
|
|
2180
2649
|
// TODO: needs to be optimized
|
|
2181
|
-
async finishAuth(authCode) {
|
|
2650
|
+
async finishAuth(authCode, state) {
|
|
2182
2651
|
this.emitStateChange("AUTHENTICATING");
|
|
2183
2652
|
this.emitProgress("Exchanging authorization code for tokens...");
|
|
2184
2653
|
await this.initialize();
|
|
@@ -2188,6 +2657,16 @@ var MCPClient = class {
|
|
|
2188
2657
|
this.emitStateChange("FAILED");
|
|
2189
2658
|
throw new Error(error);
|
|
2190
2659
|
}
|
|
2660
|
+
if (state) {
|
|
2661
|
+
const stateCheck = await this.oauthProvider.checkState(state);
|
|
2662
|
+
if (!stateCheck.valid) {
|
|
2663
|
+
const error = stateCheck.error || "Invalid OAuth state";
|
|
2664
|
+
this.emitError(error, "auth");
|
|
2665
|
+
this.emitStateChange("FAILED");
|
|
2666
|
+
throw new Error(error);
|
|
2667
|
+
}
|
|
2668
|
+
await this.oauthProvider.consumeState(state);
|
|
2669
|
+
}
|
|
2191
2670
|
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2192
2671
|
let lastError;
|
|
2193
2672
|
let tokensExchanged = false;
|
|
@@ -2227,8 +2706,8 @@ var MCPClient = class {
|
|
|
2227
2706
|
await this.client.connect(this.transport);
|
|
2228
2707
|
this.transportType = currentType;
|
|
2229
2708
|
this.emitStateChange("CONNECTED");
|
|
2230
|
-
console.log(`[MCPClient]
|
|
2231
|
-
await this.saveSession(
|
|
2709
|
+
console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
|
|
2710
|
+
await this.saveSession("active");
|
|
2232
2711
|
return;
|
|
2233
2712
|
} catch (error) {
|
|
2234
2713
|
lastError = error;
|
|
@@ -2241,12 +2720,14 @@ var MCPClient = class {
|
|
|
2241
2720
|
const msg = error instanceof Error ? error.message : "Authentication failed";
|
|
2242
2721
|
this.emitError(msg, "auth");
|
|
2243
2722
|
this.emitStateChange("FAILED");
|
|
2723
|
+
await this.deleteTransientSession();
|
|
2244
2724
|
throw error;
|
|
2245
2725
|
}
|
|
2246
2726
|
if (isLastAttempt) {
|
|
2247
2727
|
const msg = error instanceof Error ? error.message : "Authentication failed";
|
|
2248
2728
|
this.emitError(msg, "auth");
|
|
2249
2729
|
this.emitStateChange("FAILED");
|
|
2730
|
+
await this.deleteTransientSession();
|
|
2250
2731
|
throw error;
|
|
2251
2732
|
}
|
|
2252
2733
|
this.emitProgress(`Auth attempt with ${currentType} failed: ${errorMessage}. Retrying...`);
|
|
@@ -2256,6 +2737,7 @@ var MCPClient = class {
|
|
|
2256
2737
|
const errorMessage = lastError instanceof Error ? lastError.message : "Authentication failed";
|
|
2257
2738
|
this.emitError(errorMessage, "auth");
|
|
2258
2739
|
this.emitStateChange("FAILED");
|
|
2740
|
+
await this.deleteTransientSession();
|
|
2259
2741
|
throw lastError;
|
|
2260
2742
|
}
|
|
2261
2743
|
}
|
|
@@ -2477,7 +2959,7 @@ var MCPClient = class {
|
|
|
2477
2959
|
await this.oauthProvider.invalidateCredentials("all");
|
|
2478
2960
|
}
|
|
2479
2961
|
await sessions.delete(this.userId, this.sessionId);
|
|
2480
|
-
this.disconnect();
|
|
2962
|
+
await this.disconnect();
|
|
2481
2963
|
}
|
|
2482
2964
|
/**
|
|
2483
2965
|
* Checks if the client is currently connected to an MCP server
|
|
@@ -2487,10 +2969,21 @@ var MCPClient = class {
|
|
|
2487
2969
|
return this.client !== null;
|
|
2488
2970
|
}
|
|
2489
2971
|
/**
|
|
2490
|
-
* Disconnects from the MCP server and cleans up resources
|
|
2491
|
-
* Does not remove session from Redis
|
|
2972
|
+
* Disconnects from the MCP server and cleans up resources.
|
|
2973
|
+
* Does not remove session from Redis — use clearSession() for that.
|
|
2974
|
+
*
|
|
2975
|
+
* For Streamable HTTP sessions, sends an HTTP DELETE to the MCP endpoint
|
|
2976
|
+
* before closing, as recommended by the MCP Streamable HTTP spec
|
|
2977
|
+
* (section "Session Management", rule 5). This is best-effort — errors
|
|
2978
|
+
* (e.g. server already restarted, 404/405 responses) are silently ignored.
|
|
2492
2979
|
*/
|
|
2493
|
-
disconnect(
|
|
2980
|
+
async disconnect() {
|
|
2981
|
+
if (this.transport instanceof streamableHttp_js.StreamableHTTPClientTransport) {
|
|
2982
|
+
try {
|
|
2983
|
+
await this.transport.terminateSession();
|
|
2984
|
+
} catch {
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2494
2987
|
if (this.client) {
|
|
2495
2988
|
this.client.close();
|
|
2496
2989
|
}
|
|
@@ -2502,7 +2995,6 @@ var MCPClient = class {
|
|
|
2502
2995
|
type: "disconnected",
|
|
2503
2996
|
sessionId: this.sessionId,
|
|
2504
2997
|
serverId: this.serverId,
|
|
2505
|
-
reason,
|
|
2506
2998
|
timestamp: Date.now()
|
|
2507
2999
|
});
|
|
2508
3000
|
this._onObservabilityEvent.fire({
|
|
@@ -2511,9 +3003,7 @@ var MCPClient = class {
|
|
|
2511
3003
|
message: `Disconnected from ${this.serverId}`,
|
|
2512
3004
|
sessionId: this.sessionId,
|
|
2513
3005
|
serverId: this.serverId,
|
|
2514
|
-
payload: {
|
|
2515
|
-
reason: reason || "unknown"
|
|
2516
|
-
},
|
|
3006
|
+
payload: {},
|
|
2517
3007
|
timestamp: Date.now(),
|
|
2518
3008
|
id: nanoid.nanoid()
|
|
2519
3009
|
});
|
|
@@ -2570,48 +3060,6 @@ var MCPClient = class {
|
|
|
2570
3060
|
getSessionId() {
|
|
2571
3061
|
return this.sessionId;
|
|
2572
3062
|
}
|
|
2573
|
-
/**
|
|
2574
|
-
* Gets MCP server configuration for all active user sessions
|
|
2575
|
-
* Loads sessions from storage and returns server connection metadata.
|
|
2576
|
-
* OAuth refresh is handled by SDK transports through their authProvider.
|
|
2577
|
-
* @deprecated This returns legacy connection metadata only and does not
|
|
2578
|
-
* include OAuth tokens or generated Authorization headers. Prefer
|
|
2579
|
-
* MultiSessionClient or explicit MCPClient instances so SDK transports can
|
|
2580
|
-
* own OAuth refresh and reauthorization.
|
|
2581
|
-
* @param userId - User ID to fetch sessions for
|
|
2582
|
-
* @returns Object keyed by sanitized server labels containing transport and url.
|
|
2583
|
-
* @static
|
|
2584
|
-
*/
|
|
2585
|
-
static async getMcpServerConfig(userId) {
|
|
2586
|
-
const mcpConfig = {};
|
|
2587
|
-
const sessionList = await sessions.list(userId);
|
|
2588
|
-
await Promise.all(
|
|
2589
|
-
sessionList.map(async (sessionData) => {
|
|
2590
|
-
const { sessionId } = sessionData;
|
|
2591
|
-
try {
|
|
2592
|
-
if (!sessionData.serverId || !sessionData.transportType || !sessionData.serverUrl || !sessionData.callbackUrl) {
|
|
2593
|
-
await sessions.delete(userId, sessionId);
|
|
2594
|
-
return;
|
|
2595
|
-
}
|
|
2596
|
-
const label = sanitizeServerLabel(
|
|
2597
|
-
sessionData.serverName || sessionData.serverId || "server"
|
|
2598
|
-
);
|
|
2599
|
-
mcpConfig[label] = {
|
|
2600
|
-
transport: sessionData.transportType,
|
|
2601
|
-
url: sessionData.serverUrl,
|
|
2602
|
-
...sessionData.serverName && {
|
|
2603
|
-
serverName: sessionData.serverName,
|
|
2604
|
-
serverLabel: label
|
|
2605
|
-
}
|
|
2606
|
-
};
|
|
2607
|
-
} catch (error) {
|
|
2608
|
-
await sessions.delete(userId, sessionId);
|
|
2609
|
-
console.warn(`[MCP] Failed to process session ${sessionId}:`, error);
|
|
2610
|
-
}
|
|
2611
|
-
})
|
|
2612
|
-
);
|
|
2613
|
-
return mcpConfig;
|
|
2614
|
-
}
|
|
2615
3063
|
};
|
|
2616
3064
|
|
|
2617
3065
|
// src/server/mcp/multi-session-client.ts
|
|
@@ -2647,18 +3095,13 @@ var MultiSessionClient = class {
|
|
|
2647
3095
|
*
|
|
2648
3096
|
* A session is considered connectable when:
|
|
2649
3097
|
* - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
|
|
2650
|
-
* - Its
|
|
2651
|
-
* either mid-OAuth flow, auth-pending, or previously failed. We skip those here
|
|
3098
|
+
* - Its status is `active`. Pending sessions are skipped here
|
|
2652
3099
|
* and let the OAuth flow complete separately before we try to reconnect them.
|
|
2653
|
-
*
|
|
2654
|
-
* Note: Sessions where `active` is `undefined` (legacy records) are included
|
|
2655
|
-
* for backwards compatibility.
|
|
2656
3100
|
*/
|
|
2657
3101
|
async getActiveSessions() {
|
|
2658
3102
|
const sessionList = await sessions.list(this.userId);
|
|
2659
3103
|
const valid = sessionList.filter(
|
|
2660
|
-
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.
|
|
2661
|
-
// exclude OAuth-pending / failed sessions
|
|
3104
|
+
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
|
|
2662
3105
|
);
|
|
2663
3106
|
return valid;
|
|
2664
3107
|
}
|
|
@@ -2679,6 +3122,9 @@ var MultiSessionClient = class {
|
|
|
2679
3122
|
* Connects a single session, with built-in deduplication to prevent race conditions.
|
|
2680
3123
|
*
|
|
2681
3124
|
* - If a client for this session already exists and is connected, returns immediately.
|
|
3125
|
+
* - If the existing client entry is no longer connected (e.g. it was explicitly
|
|
3126
|
+
* disconnected), it is evicted so that `establishConnectionWithRetries` creates a
|
|
3127
|
+
* fresh transport — preventing "Client already connected" errors from the SDK.
|
|
2682
3128
|
* - If a connection attempt for this session is already in-flight (e.g. from a
|
|
2683
3129
|
* concurrent call), it joins the existing promise instead of starting a new one.
|
|
2684
3130
|
* This is the key concurrency lock — the `connectionPromises` map acts as a
|
|
@@ -2686,9 +3132,12 @@ var MultiSessionClient = class {
|
|
|
2686
3132
|
* - On completion (success or failure), the promise is cleaned up from the map.
|
|
2687
3133
|
*/
|
|
2688
3134
|
async connectSession(session) {
|
|
2689
|
-
const
|
|
2690
|
-
if (
|
|
2691
|
-
|
|
3135
|
+
const existing = this.clients.find((c) => c.getSessionId() === session.sessionId);
|
|
3136
|
+
if (existing) {
|
|
3137
|
+
if (existing.isConnected()) {
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
this.clients = this.clients.filter((c) => c !== existing);
|
|
2692
3141
|
}
|
|
2693
3142
|
if (this.connectionPromises.has(session.sessionId)) {
|
|
2694
3143
|
return this.connectionPromises.get(session.sessionId);
|
|
@@ -2763,8 +3212,24 @@ var MultiSessionClient = class {
|
|
|
2763
3212
|
*/
|
|
2764
3213
|
async connect() {
|
|
2765
3214
|
const sessions2 = await this.getActiveSessions();
|
|
3215
|
+
const activeSessionIds = new Set(sessions2.map((s) => s.sessionId));
|
|
3216
|
+
this.clients = this.clients.filter((c) => activeSessionIds.has(c.getSessionId()));
|
|
2766
3217
|
await this.connectInBatches(sessions2);
|
|
2767
3218
|
}
|
|
3219
|
+
/**
|
|
3220
|
+
* Drops all cached `MCPClient` instances and reconnects fresh from storage.
|
|
3221
|
+
*
|
|
3222
|
+
* Call this when downstream MCP servers have expired their transport sessions
|
|
3223
|
+
* (e.g. after a remote server restart) and subsequent tool calls return
|
|
3224
|
+
* "Session not found. Reconnect without session header." errors.
|
|
3225
|
+
*
|
|
3226
|
+
* OAuth tokens are preserved in the storage backend — no re-authentication
|
|
3227
|
+
* is required. Only the in-memory transport sessions are cleared.
|
|
3228
|
+
*/
|
|
3229
|
+
async reconnect() {
|
|
3230
|
+
await this.disconnect();
|
|
3231
|
+
await this.connect();
|
|
3232
|
+
}
|
|
2768
3233
|
/**
|
|
2769
3234
|
* Returns all currently connected `MCPClient` instances.
|
|
2770
3235
|
*
|
|
@@ -2777,11 +3242,15 @@ var MultiSessionClient = class {
|
|
|
2777
3242
|
/**
|
|
2778
3243
|
* Gracefully disconnects all active MCP clients and clears the internal client list.
|
|
2779
3244
|
*
|
|
3245
|
+
* For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
|
|
3246
|
+
* endpoint per the spec before closing locally. All disconnects run in
|
|
3247
|
+
* parallel so shutdown is not serialised across many sessions.
|
|
3248
|
+
*
|
|
2780
3249
|
* Call this during server shutdown or when a user logs out to free up
|
|
2781
3250
|
* underlying transport resources (SSE streams, HTTP connections, etc.).
|
|
2782
3251
|
*/
|
|
2783
|
-
disconnect() {
|
|
2784
|
-
this.clients.
|
|
3252
|
+
async disconnect() {
|
|
3253
|
+
await Promise.all(this.clients.map((client) => client.disconnect()));
|
|
2785
3254
|
this.clients = [];
|
|
2786
3255
|
}
|
|
2787
3256
|
};
|
|
@@ -2933,7 +3402,8 @@ var SSEConnectionManager = class {
|
|
|
2933
3402
|
serverUrl: s.serverUrl,
|
|
2934
3403
|
transport: s.transportType,
|
|
2935
3404
|
createdAt: s.createdAt,
|
|
2936
|
-
|
|
3405
|
+
updatedAt: s.updatedAt ?? s.createdAt,
|
|
3406
|
+
status: s.status ?? "pending"
|
|
2937
3407
|
}))
|
|
2938
3408
|
};
|
|
2939
3409
|
}
|
|
@@ -2949,7 +3419,7 @@ var SSEConnectionManager = class {
|
|
|
2949
3419
|
(s) => s.serverId === serverId || s.serverUrl === serverUrl
|
|
2950
3420
|
);
|
|
2951
3421
|
if (duplicate) {
|
|
2952
|
-
if (duplicate.
|
|
3422
|
+
if (duplicate.status === "pending") {
|
|
2953
3423
|
await this.getSession({ sessionId: duplicate.sessionId });
|
|
2954
3424
|
return {
|
|
2955
3425
|
sessionId: duplicate.sessionId,
|
|
@@ -3014,7 +3484,6 @@ var SSEConnectionManager = class {
|
|
|
3014
3484
|
const client = this.clients.get(sessionId);
|
|
3015
3485
|
if (client) {
|
|
3016
3486
|
await client.clearSession();
|
|
3017
|
-
client.disconnect();
|
|
3018
3487
|
this.clients.delete(sessionId);
|
|
3019
3488
|
} else {
|
|
3020
3489
|
await sessions.delete(this.userId, sessionId);
|
|
@@ -3136,7 +3605,10 @@ var SSEConnectionManager = class {
|
|
|
3136
3605
|
* Complete OAuth authorization flow
|
|
3137
3606
|
*/
|
|
3138
3607
|
async finishAuth(params) {
|
|
3139
|
-
const {
|
|
3608
|
+
const { code } = params;
|
|
3609
|
+
const oauthState = params.state;
|
|
3610
|
+
const parsedState = parseOAuthState(oauthState);
|
|
3611
|
+
const sessionId = parsedState?.sessionId || oauthState;
|
|
3140
3612
|
const session = await sessions.get(this.userId, sessionId);
|
|
3141
3613
|
if (!session) {
|
|
3142
3614
|
throw new Error("Session not found");
|
|
@@ -3161,7 +3633,7 @@ var SSEConnectionManager = class {
|
|
|
3161
3633
|
headers: session.headers
|
|
3162
3634
|
});
|
|
3163
3635
|
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
3164
|
-
await client.finishAuth(code);
|
|
3636
|
+
await client.finishAuth(code, oauthState);
|
|
3165
3637
|
this.clients.set(sessionId, client);
|
|
3166
3638
|
const tools = await client.listTools();
|
|
3167
3639
|
return { success: true, toolCount: tools.tools.length };
|
|
@@ -3220,14 +3692,14 @@ var SSEConnectionManager = class {
|
|
|
3220
3692
|
/**
|
|
3221
3693
|
* Cleanup and close all connections
|
|
3222
3694
|
*/
|
|
3223
|
-
dispose() {
|
|
3695
|
+
async dispose() {
|
|
3224
3696
|
this.isActive = false;
|
|
3225
3697
|
if (this.heartbeatTimer) {
|
|
3226
3698
|
clearInterval(this.heartbeatTimer);
|
|
3227
3699
|
}
|
|
3228
|
-
|
|
3229
|
-
client.disconnect()
|
|
3230
|
-
|
|
3700
|
+
await Promise.all(
|
|
3701
|
+
Array.from(this.clients.values()).map((client) => client.disconnect())
|
|
3702
|
+
);
|
|
3231
3703
|
this.clients.clear();
|
|
3232
3704
|
}
|
|
3233
3705
|
};
|
|
@@ -3473,8 +3945,8 @@ var SSEClient = class {
|
|
|
3473
3945
|
async getSession(sessionId) {
|
|
3474
3946
|
return this.sendRequest("getSession", { sessionId });
|
|
3475
3947
|
}
|
|
3476
|
-
async finishAuth(
|
|
3477
|
-
return this.sendRequest("finishAuth", {
|
|
3948
|
+
async finishAuth(state, code) {
|
|
3949
|
+
return this.sendRequest("finishAuth", { state, code });
|
|
3478
3950
|
}
|
|
3479
3951
|
async listPrompts(sessionId) {
|
|
3480
3952
|
return this.sendRequest("listPrompts", { sessionId });
|
|
@@ -5357,6 +5829,8 @@ exports.DEFAULT_HEARTBEAT_INTERVAL_MS = DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
|
5357
5829
|
exports.DEFAULT_LOGO_URI = DEFAULT_LOGO_URI;
|
|
5358
5830
|
exports.DEFAULT_MCP_APP_CSP = DEFAULT_MCP_APP_CSP;
|
|
5359
5831
|
exports.DEFAULT_POLICY_URI = DEFAULT_POLICY_URI;
|
|
5832
|
+
exports.DORMANT_SESSION_EXPIRATION_MS = DORMANT_SESSION_EXPIRATION_MS;
|
|
5833
|
+
exports.DORMANT_SESSION_EXPIRATION_SECONDS = DORMANT_SESSION_EXPIRATION_SECONDS;
|
|
5360
5834
|
exports.DisposableStore = DisposableStore;
|
|
5361
5835
|
exports.Emitter = Emitter;
|
|
5362
5836
|
exports.InvalidStateError = InvalidStateError;
|
|
@@ -5366,11 +5840,11 @@ exports.MCP_CLIENT_VERSION = MCP_CLIENT_VERSION;
|
|
|
5366
5840
|
exports.McpError = McpError;
|
|
5367
5841
|
exports.MultiSessionClient = MultiSessionClient;
|
|
5368
5842
|
exports.NotConnectedError = NotConnectedError;
|
|
5843
|
+
exports.PENDING_SESSION_EXPIRATION_SECONDS = PENDING_SESSION_EXPIRATION_SECONDS;
|
|
5369
5844
|
exports.REDIS_KEY_PREFIX = REDIS_KEY_PREFIX;
|
|
5370
5845
|
exports.RpcErrorCodes = RpcErrorCodes;
|
|
5371
5846
|
exports.SANDBOX_PROXY_READY_METHOD = SANDBOX_PROXY_READY_METHOD;
|
|
5372
5847
|
exports.SANDBOX_RESOURCE_READY_METHOD = SANDBOX_RESOURCE_READY_METHOD;
|
|
5373
|
-
exports.SESSION_TTL_SECONDS = SESSION_TTL_SECONDS;
|
|
5374
5848
|
exports.SOFTWARE_ID = SOFTWARE_ID;
|
|
5375
5849
|
exports.SOFTWARE_VERSION = SOFTWARE_VERSION;
|
|
5376
5850
|
exports.SSEClient = SSEClient;
|
|
@@ -5380,7 +5854,6 @@ exports.SchemaCompressor = SchemaCompressor;
|
|
|
5380
5854
|
exports.SessionNotFoundError = SessionNotFoundError;
|
|
5381
5855
|
exports.SessionValidationError = SessionValidationError;
|
|
5382
5856
|
exports.StorageOAuthClientProvider = StorageOAuthClientProvider;
|
|
5383
|
-
exports.TOKEN_EXPIRY_BUFFER_MS = TOKEN_EXPIRY_BUFFER_MS;
|
|
5384
5857
|
exports.ToolExecutionError = ToolExecutionError;
|
|
5385
5858
|
exports.ToolIndex = ToolIndex;
|
|
5386
5859
|
exports.ToolRouter = ToolRouter;
|