@mcp-ts/sdk 2.3.4 → 2.4.0
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 +113 -30
- package/dist/adapters/agui-adapter.d.mts +3 -3
- package/dist/adapters/agui-adapter.d.ts +3 -3
- package/dist/adapters/agui-middleware.d.mts +3 -3
- package/dist/adapters/agui-middleware.d.ts +3 -3
- package/dist/adapters/ai-adapter.d.mts +3 -3
- package/dist/adapters/ai-adapter.d.ts +3 -3
- package/dist/adapters/langchain-adapter.d.mts +3 -3
- package/dist/adapters/langchain-adapter.d.ts +3 -3
- package/dist/adapters/mastra-adapter.d.mts +1 -1
- package/dist/adapters/mastra-adapter.d.ts +1 -1
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- 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 +9 -8
- package/dist/client/react.d.ts +9 -8
- 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 +6 -5
- package/dist/client/vue.d.ts +6 -5
- 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/{index-Cfjsme-a.d.mts → index-ByIjEReo.d.mts} +2 -2
- package/dist/{index-CmjMd2ac.d.ts → index-CtXvKl8N.d.ts} +2 -2
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +728 -306
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +722 -304
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-C7hGqzgM.d.mts → multi-session-client-CIMUGF8S.d.mts} +15 -33
- package/dist/{multi-session-client-C_kPHpD5.d.ts → multi-session-client-CnvZEGPY.d.ts} +15 -33
- package/dist/server/index.d.mts +45 -17
- package/dist/server/index.d.ts +45 -17
- package/dist/server/index.js +720 -300
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +716 -299
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +9 -8
- package/dist/shared/index.d.ts +9 -8
- 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/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 +9 -4
- package/src/server/mcp/multi-session-client.ts +2 -6
- package/src/server/mcp/oauth-client.ts +73 -101
- 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/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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1133
|
+
authUrl: row.auth_url,
|
|
1134
|
+
status: row.status ?? "pending"
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
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
|
|
882
1146
|
};
|
|
883
1147
|
}
|
|
884
|
-
|
|
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
|
|
|
@@ -1958,17 +2392,18 @@ var MCPClient = class {
|
|
|
1958
2392
|
}
|
|
1959
2393
|
this.emitStateChange("INITIALIZING");
|
|
1960
2394
|
this.emitProgress("Loading session configuration...");
|
|
2395
|
+
let existingSession = null;
|
|
1961
2396
|
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
1962
|
-
|
|
1963
|
-
if (!
|
|
2397
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2398
|
+
if (!existingSession) {
|
|
1964
2399
|
throw new Error(`Session not found: ${this.sessionId}`);
|
|
1965
2400
|
}
|
|
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 =
|
|
2401
|
+
this.serverUrl = this.serverUrl || existingSession.serverUrl;
|
|
2402
|
+
this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
|
|
2403
|
+
this.serverName = this.serverName || existingSession.serverName;
|
|
2404
|
+
this.serverId = this.serverId || existingSession.serverId || "unknown";
|
|
2405
|
+
this.headers = this.headers || existingSession.headers;
|
|
2406
|
+
this.createdAt = existingSession.createdAt;
|
|
1972
2407
|
}
|
|
1973
2408
|
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
1974
2409
|
throw new Error("Missing required connection metadata");
|
|
@@ -2012,10 +2447,13 @@ var MCPClient = class {
|
|
|
2012
2447
|
}
|
|
2013
2448
|
);
|
|
2014
2449
|
}
|
|
2015
|
-
|
|
2450
|
+
if (existingSession === null) {
|
|
2451
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2452
|
+
}
|
|
2016
2453
|
if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
|
|
2017
2454
|
this.createdAt = Date.now();
|
|
2018
|
-
|
|
2455
|
+
const updatedAt = this.createdAt;
|
|
2456
|
+
console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
|
|
2019
2457
|
await sessions.create({
|
|
2020
2458
|
sessionId: this.sessionId,
|
|
2021
2459
|
userId: this.userId,
|
|
@@ -2026,18 +2464,18 @@ var MCPClient = class {
|
|
|
2026
2464
|
transportType: this.transportType || "streamable-http",
|
|
2027
2465
|
headers: this.headers,
|
|
2028
2466
|
createdAt: this.createdAt,
|
|
2029
|
-
|
|
2030
|
-
|
|
2467
|
+
updatedAt,
|
|
2468
|
+
status: "pending"
|
|
2469
|
+
});
|
|
2031
2470
|
}
|
|
2032
2471
|
}
|
|
2033
2472
|
/**
|
|
2034
2473
|
* Saves current session state to the session store
|
|
2035
2474
|
* 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
|
|
2475
|
+
* @param status - Session lifecycle status used by storage cleanup
|
|
2038
2476
|
* @private
|
|
2039
2477
|
*/
|
|
2040
|
-
async saveSession(
|
|
2478
|
+
async saveSession(status = "active", existingSession) {
|
|
2041
2479
|
if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
|
|
2042
2480
|
return;
|
|
2043
2481
|
}
|
|
@@ -2051,13 +2489,29 @@ var MCPClient = class {
|
|
|
2051
2489
|
transportType: this.transportType || "streamable-http",
|
|
2052
2490
|
headers: this.headers,
|
|
2053
2491
|
createdAt: this.createdAt || Date.now(),
|
|
2054
|
-
|
|
2492
|
+
updatedAt: Date.now(),
|
|
2493
|
+
status
|
|
2055
2494
|
};
|
|
2056
|
-
|
|
2495
|
+
if (status === "active") {
|
|
2496
|
+
sessionData.authUrl = null;
|
|
2497
|
+
}
|
|
2498
|
+
if (existingSession === void 0) {
|
|
2499
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2500
|
+
}
|
|
2057
2501
|
if (existingSession) {
|
|
2058
|
-
await sessions.update(this.userId, this.sessionId, sessionData
|
|
2502
|
+
await sessions.update(this.userId, this.sessionId, sessionData);
|
|
2059
2503
|
} else {
|
|
2060
|
-
await sessions.create(sessionData
|
|
2504
|
+
await sessions.create(sessionData);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
/**
|
|
2508
|
+
* Removes transient setup/auth sessions without masking the original error.
|
|
2509
|
+
* @private
|
|
2510
|
+
*/
|
|
2511
|
+
async deleteTransientSession() {
|
|
2512
|
+
try {
|
|
2513
|
+
await sessions.delete(this.userId, this.sessionId);
|
|
2514
|
+
} catch {
|
|
2061
2515
|
}
|
|
2062
2516
|
}
|
|
2063
2517
|
/**
|
|
@@ -2122,8 +2576,8 @@ var MCPClient = class {
|
|
|
2122
2576
|
this.transportType = transportType;
|
|
2123
2577
|
this.emitStateChange("CONNECTED");
|
|
2124
2578
|
this.emitProgress("Connected successfully");
|
|
2125
|
-
console.log(`[MCPClient] Saving session ${this.sessionId}
|
|
2126
|
-
await this.saveSession(
|
|
2579
|
+
console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
|
|
2580
|
+
await this.saveSession("active");
|
|
2127
2581
|
} catch (error) {
|
|
2128
2582
|
if (error instanceof auth_js.UnauthorizedError || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
|
|
2129
2583
|
let authUrl = "";
|
|
@@ -2135,15 +2589,12 @@ var MCPClient = class {
|
|
|
2135
2589
|
const message = detail.toLowerCase() === "unauthorized" ? "OAuth authorization URL not available" : `OAuth authorization URL not available: ${detail}`;
|
|
2136
2590
|
this.emitError(message, "auth");
|
|
2137
2591
|
this.emitStateChange("FAILED");
|
|
2138
|
-
|
|
2139
|
-
await sessions.delete(this.userId, this.sessionId);
|
|
2140
|
-
} catch {
|
|
2141
|
-
}
|
|
2592
|
+
await this.deleteTransientSession();
|
|
2142
2593
|
throw new Error(message);
|
|
2143
2594
|
}
|
|
2144
2595
|
this.emitStateChange("AUTHENTICATING");
|
|
2145
|
-
console.log(`[MCPClient] Saving session ${this.sessionId}
|
|
2146
|
-
await this.saveSession(
|
|
2596
|
+
console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
|
|
2597
|
+
await this.saveSession("pending");
|
|
2147
2598
|
if (this.serverId) {
|
|
2148
2599
|
this._onConnectionEvent.fire({
|
|
2149
2600
|
type: "auth_required",
|
|
@@ -2163,7 +2614,7 @@ var MCPClient = class {
|
|
|
2163
2614
|
this.emitStateChange("FAILED");
|
|
2164
2615
|
try {
|
|
2165
2616
|
const existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2166
|
-
if (!existingSession || existingSession.
|
|
2617
|
+
if (!existingSession || existingSession.status !== "active") {
|
|
2167
2618
|
await sessions.delete(this.userId, this.sessionId);
|
|
2168
2619
|
}
|
|
2169
2620
|
} catch {
|
|
@@ -2178,7 +2629,7 @@ var MCPClient = class {
|
|
|
2178
2629
|
* @param authCode - Authorization code received from OAuth callback
|
|
2179
2630
|
*/
|
|
2180
2631
|
// TODO: needs to be optimized
|
|
2181
|
-
async finishAuth(authCode) {
|
|
2632
|
+
async finishAuth(authCode, state) {
|
|
2182
2633
|
this.emitStateChange("AUTHENTICATING");
|
|
2183
2634
|
this.emitProgress("Exchanging authorization code for tokens...");
|
|
2184
2635
|
await this.initialize();
|
|
@@ -2188,6 +2639,16 @@ var MCPClient = class {
|
|
|
2188
2639
|
this.emitStateChange("FAILED");
|
|
2189
2640
|
throw new Error(error);
|
|
2190
2641
|
}
|
|
2642
|
+
if (state) {
|
|
2643
|
+
const stateCheck = await this.oauthProvider.checkState(state);
|
|
2644
|
+
if (!stateCheck.valid) {
|
|
2645
|
+
const error = stateCheck.error || "Invalid OAuth state";
|
|
2646
|
+
this.emitError(error, "auth");
|
|
2647
|
+
this.emitStateChange("FAILED");
|
|
2648
|
+
throw new Error(error);
|
|
2649
|
+
}
|
|
2650
|
+
await this.oauthProvider.consumeState(state);
|
|
2651
|
+
}
|
|
2191
2652
|
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2192
2653
|
let lastError;
|
|
2193
2654
|
let tokensExchanged = false;
|
|
@@ -2227,8 +2688,8 @@ var MCPClient = class {
|
|
|
2227
2688
|
await this.client.connect(this.transport);
|
|
2228
2689
|
this.transportType = currentType;
|
|
2229
2690
|
this.emitStateChange("CONNECTED");
|
|
2230
|
-
console.log(`[MCPClient]
|
|
2231
|
-
await this.saveSession(
|
|
2691
|
+
console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
|
|
2692
|
+
await this.saveSession("active");
|
|
2232
2693
|
return;
|
|
2233
2694
|
} catch (error) {
|
|
2234
2695
|
lastError = error;
|
|
@@ -2241,12 +2702,14 @@ var MCPClient = class {
|
|
|
2241
2702
|
const msg = error instanceof Error ? error.message : "Authentication failed";
|
|
2242
2703
|
this.emitError(msg, "auth");
|
|
2243
2704
|
this.emitStateChange("FAILED");
|
|
2705
|
+
await this.deleteTransientSession();
|
|
2244
2706
|
throw error;
|
|
2245
2707
|
}
|
|
2246
2708
|
if (isLastAttempt) {
|
|
2247
2709
|
const msg = error instanceof Error ? error.message : "Authentication failed";
|
|
2248
2710
|
this.emitError(msg, "auth");
|
|
2249
2711
|
this.emitStateChange("FAILED");
|
|
2712
|
+
await this.deleteTransientSession();
|
|
2250
2713
|
throw error;
|
|
2251
2714
|
}
|
|
2252
2715
|
this.emitProgress(`Auth attempt with ${currentType} failed: ${errorMessage}. Retrying...`);
|
|
@@ -2256,6 +2719,7 @@ var MCPClient = class {
|
|
|
2256
2719
|
const errorMessage = lastError instanceof Error ? lastError.message : "Authentication failed";
|
|
2257
2720
|
this.emitError(errorMessage, "auth");
|
|
2258
2721
|
this.emitStateChange("FAILED");
|
|
2722
|
+
await this.deleteTransientSession();
|
|
2259
2723
|
throw lastError;
|
|
2260
2724
|
}
|
|
2261
2725
|
}
|
|
@@ -2570,48 +3034,6 @@ var MCPClient = class {
|
|
|
2570
3034
|
getSessionId() {
|
|
2571
3035
|
return this.sessionId;
|
|
2572
3036
|
}
|
|
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
3037
|
};
|
|
2616
3038
|
|
|
2617
3039
|
// src/server/mcp/multi-session-client.ts
|
|
@@ -2647,18 +3069,13 @@ var MultiSessionClient = class {
|
|
|
2647
3069
|
*
|
|
2648
3070
|
* A session is considered connectable when:
|
|
2649
3071
|
* - 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
|
|
3072
|
+
* - Its status is `active`. Pending sessions are skipped here
|
|
2652
3073
|
* 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
3074
|
*/
|
|
2657
3075
|
async getActiveSessions() {
|
|
2658
3076
|
const sessionList = await sessions.list(this.userId);
|
|
2659
3077
|
const valid = sessionList.filter(
|
|
2660
|
-
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.
|
|
2661
|
-
// exclude OAuth-pending / failed sessions
|
|
3078
|
+
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
|
|
2662
3079
|
);
|
|
2663
3080
|
return valid;
|
|
2664
3081
|
}
|
|
@@ -2933,7 +3350,8 @@ var SSEConnectionManager = class {
|
|
|
2933
3350
|
serverUrl: s.serverUrl,
|
|
2934
3351
|
transport: s.transportType,
|
|
2935
3352
|
createdAt: s.createdAt,
|
|
2936
|
-
|
|
3353
|
+
updatedAt: s.updatedAt ?? s.createdAt,
|
|
3354
|
+
status: s.status ?? "pending"
|
|
2937
3355
|
}))
|
|
2938
3356
|
};
|
|
2939
3357
|
}
|
|
@@ -2949,7 +3367,7 @@ var SSEConnectionManager = class {
|
|
|
2949
3367
|
(s) => s.serverId === serverId || s.serverUrl === serverUrl
|
|
2950
3368
|
);
|
|
2951
3369
|
if (duplicate) {
|
|
2952
|
-
if (duplicate.
|
|
3370
|
+
if (duplicate.status === "pending") {
|
|
2953
3371
|
await this.getSession({ sessionId: duplicate.sessionId });
|
|
2954
3372
|
return {
|
|
2955
3373
|
sessionId: duplicate.sessionId,
|
|
@@ -3136,7 +3554,10 @@ var SSEConnectionManager = class {
|
|
|
3136
3554
|
* Complete OAuth authorization flow
|
|
3137
3555
|
*/
|
|
3138
3556
|
async finishAuth(params) {
|
|
3139
|
-
const {
|
|
3557
|
+
const { code } = params;
|
|
3558
|
+
const oauthState = params.state;
|
|
3559
|
+
const parsedState = parseOAuthState(oauthState);
|
|
3560
|
+
const sessionId = parsedState?.sessionId || oauthState;
|
|
3140
3561
|
const session = await sessions.get(this.userId, sessionId);
|
|
3141
3562
|
if (!session) {
|
|
3142
3563
|
throw new Error("Session not found");
|
|
@@ -3161,7 +3582,7 @@ var SSEConnectionManager = class {
|
|
|
3161
3582
|
headers: session.headers
|
|
3162
3583
|
});
|
|
3163
3584
|
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
3164
|
-
await client.finishAuth(code);
|
|
3585
|
+
await client.finishAuth(code, oauthState);
|
|
3165
3586
|
this.clients.set(sessionId, client);
|
|
3166
3587
|
const tools = await client.listTools();
|
|
3167
3588
|
return { success: true, toolCount: tools.tools.length };
|
|
@@ -3473,8 +3894,8 @@ var SSEClient = class {
|
|
|
3473
3894
|
async getSession(sessionId) {
|
|
3474
3895
|
return this.sendRequest("getSession", { sessionId });
|
|
3475
3896
|
}
|
|
3476
|
-
async finishAuth(
|
|
3477
|
-
return this.sendRequest("finishAuth", {
|
|
3897
|
+
async finishAuth(state, code) {
|
|
3898
|
+
return this.sendRequest("finishAuth", { state, code });
|
|
3478
3899
|
}
|
|
3479
3900
|
async listPrompts(sessionId) {
|
|
3480
3901
|
return this.sendRequest("listPrompts", { sessionId });
|
|
@@ -5357,6 +5778,8 @@ exports.DEFAULT_HEARTBEAT_INTERVAL_MS = DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
|
5357
5778
|
exports.DEFAULT_LOGO_URI = DEFAULT_LOGO_URI;
|
|
5358
5779
|
exports.DEFAULT_MCP_APP_CSP = DEFAULT_MCP_APP_CSP;
|
|
5359
5780
|
exports.DEFAULT_POLICY_URI = DEFAULT_POLICY_URI;
|
|
5781
|
+
exports.DORMANT_SESSION_EXPIRATION_MS = DORMANT_SESSION_EXPIRATION_MS;
|
|
5782
|
+
exports.DORMANT_SESSION_EXPIRATION_SECONDS = DORMANT_SESSION_EXPIRATION_SECONDS;
|
|
5360
5783
|
exports.DisposableStore = DisposableStore;
|
|
5361
5784
|
exports.Emitter = Emitter;
|
|
5362
5785
|
exports.InvalidStateError = InvalidStateError;
|
|
@@ -5366,11 +5789,11 @@ exports.MCP_CLIENT_VERSION = MCP_CLIENT_VERSION;
|
|
|
5366
5789
|
exports.McpError = McpError;
|
|
5367
5790
|
exports.MultiSessionClient = MultiSessionClient;
|
|
5368
5791
|
exports.NotConnectedError = NotConnectedError;
|
|
5792
|
+
exports.PENDING_SESSION_EXPIRATION_SECONDS = PENDING_SESSION_EXPIRATION_SECONDS;
|
|
5369
5793
|
exports.REDIS_KEY_PREFIX = REDIS_KEY_PREFIX;
|
|
5370
5794
|
exports.RpcErrorCodes = RpcErrorCodes;
|
|
5371
5795
|
exports.SANDBOX_PROXY_READY_METHOD = SANDBOX_PROXY_READY_METHOD;
|
|
5372
5796
|
exports.SANDBOX_RESOURCE_READY_METHOD = SANDBOX_RESOURCE_READY_METHOD;
|
|
5373
|
-
exports.SESSION_TTL_SECONDS = SESSION_TTL_SECONDS;
|
|
5374
5797
|
exports.SOFTWARE_ID = SOFTWARE_ID;
|
|
5375
5798
|
exports.SOFTWARE_VERSION = SOFTWARE_VERSION;
|
|
5376
5799
|
exports.SSEClient = SSEClient;
|
|
@@ -5380,7 +5803,6 @@ exports.SchemaCompressor = SchemaCompressor;
|
|
|
5380
5803
|
exports.SessionNotFoundError = SessionNotFoundError;
|
|
5381
5804
|
exports.SessionValidationError = SessionValidationError;
|
|
5382
5805
|
exports.StorageOAuthClientProvider = StorageOAuthClientProvider;
|
|
5383
|
-
exports.TOKEN_EXPIRY_BUFFER_MS = TOKEN_EXPIRY_BUFFER_MS;
|
|
5384
5806
|
exports.ToolExecutionError = ToolExecutionError;
|
|
5385
5807
|
exports.ToolIndex = ToolIndex;
|
|
5386
5808
|
exports.ToolRouter = ToolRouter;
|