@mcp-ts/sdk 2.3.3 → 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 +729 -397
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +724 -396
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-DYNe6az3.d.mts → multi-session-client-CIMUGF8S.d.mts} +15 -41
- package/dist/{multi-session-client-BYtguGJm.d.ts → multi-session-client-CnvZEGPY.d.ts} +15 -41
- package/dist/server/index.d.mts +45 -17
- package/dist/server/index.d.ts +45 -17
- package/dist/server/index.js +721 -391
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +718 -391
- 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 -220
- 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/server/index.js
CHANGED
|
@@ -156,22 +156,9 @@ init_cjs_shims();
|
|
|
156
156
|
// src/server/storage/redis-backend.ts
|
|
157
157
|
init_cjs_shims();
|
|
158
158
|
|
|
159
|
-
// src/shared/constants.ts
|
|
160
|
-
init_cjs_shims();
|
|
161
|
-
var SESSION_TTL_SECONDS = 43200;
|
|
162
|
-
var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
|
|
163
|
-
var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
164
|
-
var DEFAULT_CLIENT_NAME = "MCP Assistant";
|
|
165
|
-
var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
|
|
166
|
-
var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
|
|
167
|
-
var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
|
|
168
|
-
var SOFTWARE_ID = "@mcp-ts";
|
|
169
|
-
var SOFTWARE_VERSION = "1.3.4";
|
|
170
|
-
var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
|
|
171
|
-
var MCP_CLIENT_VERSION = "2.0";
|
|
172
|
-
|
|
173
159
|
// src/shared/utils.ts
|
|
174
160
|
init_cjs_shims();
|
|
161
|
+
var OAUTH_STATE_SEPARATOR = ".";
|
|
175
162
|
var firstChar = nanoid.customAlphabet(
|
|
176
163
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
|
177
164
|
1
|
|
@@ -190,13 +177,99 @@ function sanitizeServerLabel(name) {
|
|
|
190
177
|
function generateSessionId() {
|
|
191
178
|
return firstChar() + rest();
|
|
192
179
|
}
|
|
180
|
+
function formatOAuthState(nonce, sessionId) {
|
|
181
|
+
return `${nonce}${OAUTH_STATE_SEPARATOR}${sessionId}`;
|
|
182
|
+
}
|
|
183
|
+
function parseOAuthState(state) {
|
|
184
|
+
const separatorIndex = state.indexOf(OAUTH_STATE_SEPARATOR);
|
|
185
|
+
if (separatorIndex <= 0 || separatorIndex === state.length - 1) {
|
|
186
|
+
return void 0;
|
|
187
|
+
}
|
|
188
|
+
const nonce = state.slice(0, separatorIndex);
|
|
189
|
+
const sessionId = state.slice(separatorIndex + 1);
|
|
190
|
+
if (!nonce || !sessionId || sessionId.includes(OAUTH_STATE_SEPARATOR)) {
|
|
191
|
+
return void 0;
|
|
192
|
+
}
|
|
193
|
+
return { nonce, sessionId };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/server/storage/session-lifecycle.ts
|
|
197
|
+
init_cjs_shims();
|
|
198
|
+
|
|
199
|
+
// src/shared/constants.ts
|
|
200
|
+
init_cjs_shims();
|
|
201
|
+
var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
|
|
202
|
+
var DORMANT_SESSION_EXPIRATION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
203
|
+
var DORMANT_SESSION_EXPIRATION_SECONDS = Math.floor(DORMANT_SESSION_EXPIRATION_MS / 1e3);
|
|
204
|
+
var DEFAULT_CLIENT_NAME = "MCP Assistant";
|
|
205
|
+
var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
|
|
206
|
+
var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
|
|
207
|
+
var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
|
|
208
|
+
var SOFTWARE_ID = "@mcp-ts";
|
|
209
|
+
var SOFTWARE_VERSION = "2.3.4";
|
|
210
|
+
var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
|
|
211
|
+
var MCP_CLIENT_VERSION = "2.0";
|
|
212
|
+
|
|
213
|
+
// src/server/storage/session-lifecycle.ts
|
|
214
|
+
function resolveSessionExpiresAt(status = "pending", referenceTime = Date.now()) {
|
|
215
|
+
return status === "active" ? null : referenceTime + STATE_EXPIRATION_MS;
|
|
216
|
+
}
|
|
217
|
+
function resolveSessionRedisTtlSeconds(session) {
|
|
218
|
+
return session.status === "active" ? DORMANT_SESSION_EXPIRATION_SECONDS : Math.floor(STATE_EXPIRATION_MS / 1e3);
|
|
219
|
+
}
|
|
220
|
+
function normalizeNewSession(session, now = Date.now()) {
|
|
221
|
+
const createdAt = session.createdAt || now;
|
|
222
|
+
const updatedAt = session.updatedAt ?? createdAt;
|
|
223
|
+
const status = session.status ?? "pending";
|
|
224
|
+
return {
|
|
225
|
+
...session,
|
|
226
|
+
status,
|
|
227
|
+
createdAt,
|
|
228
|
+
updatedAt,
|
|
229
|
+
expiresAt: resolveSessionExpiresAt(status, status === "active" ? updatedAt : createdAt)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function mergeSessionUpdate(current, data, now = Date.now()) {
|
|
233
|
+
const updatedAt = data.updatedAt ?? now;
|
|
234
|
+
const updated = {
|
|
235
|
+
...current,
|
|
236
|
+
...data,
|
|
237
|
+
updatedAt
|
|
238
|
+
};
|
|
239
|
+
const status = updated.status ?? "pending";
|
|
240
|
+
return {
|
|
241
|
+
...updated,
|
|
242
|
+
status,
|
|
243
|
+
expiresAt: resolveSessionExpiresAt(status, updatedAt)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function normalizeStoredSession(session) {
|
|
247
|
+
const createdAt = session.createdAt || Date.now();
|
|
248
|
+
const updatedAt = session.updatedAt ?? createdAt;
|
|
249
|
+
const status = session.status ?? "pending";
|
|
250
|
+
const expiresAt = status === "active" ? null : session.expiresAt ?? resolveSessionExpiresAt(status, createdAt);
|
|
251
|
+
return {
|
|
252
|
+
...session,
|
|
253
|
+
status,
|
|
254
|
+
createdAt,
|
|
255
|
+
updatedAt,
|
|
256
|
+
expiresAt
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function isSessionExpired(session, now = Date.now()) {
|
|
260
|
+
const hydrated = normalizeStoredSession(session);
|
|
261
|
+
if (hydrated.status === "active") {
|
|
262
|
+
return hydrated.updatedAt !== void 0 && hydrated.updatedAt < now - DORMANT_SESSION_EXPIRATION_MS;
|
|
263
|
+
}
|
|
264
|
+
return typeof hydrated.expiresAt === "number" && hydrated.expiresAt < now;
|
|
265
|
+
}
|
|
193
266
|
|
|
194
267
|
// src/server/storage/redis-backend.ts
|
|
195
268
|
var RedisStorageBackend = class {
|
|
196
269
|
constructor(redis2) {
|
|
197
270
|
this.redis = redis2;
|
|
198
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
199
271
|
__publicField(this, "KEY_PREFIX", "mcp:session:");
|
|
272
|
+
__publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
|
|
200
273
|
__publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
|
|
201
274
|
__publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
|
|
202
275
|
}
|
|
@@ -215,6 +288,9 @@ var RedisStorageBackend = class {
|
|
|
215
288
|
getSessionKey(userId, sessionId) {
|
|
216
289
|
return `${this.KEY_PREFIX}${userId}:${sessionId}`;
|
|
217
290
|
}
|
|
291
|
+
getCredentialsKey(userId, sessionId) {
|
|
292
|
+
return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
|
|
293
|
+
}
|
|
218
294
|
/**
|
|
219
295
|
* Generates Redis key for tracking all sessions for a user
|
|
220
296
|
* @private
|
|
@@ -252,15 +328,16 @@ var RedisStorageBackend = class {
|
|
|
252
328
|
generateSessionId() {
|
|
253
329
|
return generateSessionId();
|
|
254
330
|
}
|
|
255
|
-
async create(session
|
|
331
|
+
async create(session) {
|
|
256
332
|
const { sessionId, userId } = session;
|
|
257
333
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
258
334
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
259
335
|
const userIdKey = this.getUserIdKey(userId);
|
|
260
|
-
const
|
|
336
|
+
const sessionWithLifecycle = normalizeNewSession(session);
|
|
337
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(sessionWithLifecycle);
|
|
261
338
|
const result = await this.redis.set(
|
|
262
339
|
sessionKey,
|
|
263
|
-
JSON.stringify(
|
|
340
|
+
JSON.stringify(sessionWithLifecycle),
|
|
264
341
|
"EX",
|
|
265
342
|
effectiveTtl,
|
|
266
343
|
"NX"
|
|
@@ -270,9 +347,8 @@ var RedisStorageBackend = class {
|
|
|
270
347
|
}
|
|
271
348
|
await this.redis.sadd(userIdKey, sessionId);
|
|
272
349
|
}
|
|
273
|
-
async update(userId, sessionId, data
|
|
350
|
+
async update(userId, sessionId, data) {
|
|
274
351
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
275
|
-
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
276
352
|
const script = `
|
|
277
353
|
local currentStr = redis.call("GET", KEYS[1])
|
|
278
354
|
if not currentStr then
|
|
@@ -280,26 +356,47 @@ var RedisStorageBackend = class {
|
|
|
280
356
|
end
|
|
281
357
|
|
|
282
358
|
local current = cjson.decode(currentStr)
|
|
283
|
-
local
|
|
284
|
-
|
|
285
|
-
for k,v in pairs(updates) do
|
|
286
|
-
current[k] = v
|
|
287
|
-
end
|
|
359
|
+
local updated = cjson.decode(ARGV[1])
|
|
288
360
|
|
|
289
|
-
redis.call("SET", KEYS[1], cjson.encode(
|
|
361
|
+
redis.call("SET", KEYS[1], cjson.encode(updated), "EX", ARGV[2])
|
|
290
362
|
return 1
|
|
291
363
|
`;
|
|
364
|
+
const current = await this.get(userId, sessionId);
|
|
365
|
+
if (!current) {
|
|
366
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
367
|
+
}
|
|
368
|
+
const updated = mergeSessionUpdate(current, data);
|
|
369
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
|
|
292
370
|
const result = await this.redis.eval(
|
|
293
371
|
script,
|
|
294
372
|
1,
|
|
295
373
|
sessionKey,
|
|
296
|
-
JSON.stringify(
|
|
374
|
+
JSON.stringify(updated),
|
|
297
375
|
effectiveTtl
|
|
298
376
|
);
|
|
299
377
|
if (result === 0) {
|
|
300
378
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
301
379
|
}
|
|
302
380
|
}
|
|
381
|
+
async patchCredentials(userId, sessionId, data) {
|
|
382
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
383
|
+
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
384
|
+
const sessionExists = await this.redis.exists(sessionKey);
|
|
385
|
+
if (!sessionExists) {
|
|
386
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
387
|
+
}
|
|
388
|
+
const session = await this.get(userId, sessionId);
|
|
389
|
+
const currentTtl = await this.redis.ttl(sessionKey);
|
|
390
|
+
const effectiveTtl = currentTtl > 0 && session ? currentTtl : resolveSessionRedisTtlSeconds(session ?? { status: "pending" });
|
|
391
|
+
const currentStr = await this.redis.get(credentialsKey);
|
|
392
|
+
const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
|
|
393
|
+
const credentials = { ...current, ...data, sessionId, userId };
|
|
394
|
+
if (effectiveTtl > 0) {
|
|
395
|
+
await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
|
|
396
|
+
} else {
|
|
397
|
+
await this.redis.set(credentialsKey, JSON.stringify(credentials));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
303
400
|
async get(userId, sessionId) {
|
|
304
401
|
try {
|
|
305
402
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
@@ -307,13 +404,28 @@ var RedisStorageBackend = class {
|
|
|
307
404
|
if (!sessionDataStr) {
|
|
308
405
|
return null;
|
|
309
406
|
}
|
|
310
|
-
const
|
|
311
|
-
return
|
|
407
|
+
const session = JSON.parse(sessionDataStr);
|
|
408
|
+
return normalizeStoredSession(session);
|
|
312
409
|
} catch (error) {
|
|
313
410
|
console.error("[RedisStorageBackend] Failed to get session:", error);
|
|
314
411
|
return null;
|
|
315
412
|
}
|
|
316
413
|
}
|
|
414
|
+
async getCredentials(userId, sessionId) {
|
|
415
|
+
const session = await this.get(userId, sessionId);
|
|
416
|
+
if (!session) return null;
|
|
417
|
+
const credentialsStr = await this.redis.get(this.getCredentialsKey(userId, sessionId));
|
|
418
|
+
return credentialsStr ? JSON.parse(credentialsStr) : { sessionId, userId };
|
|
419
|
+
}
|
|
420
|
+
async clearCredentials(userId, sessionId) {
|
|
421
|
+
await this.patchCredentials(userId, sessionId, {
|
|
422
|
+
clientInformation: null,
|
|
423
|
+
tokens: null,
|
|
424
|
+
codeVerifier: null,
|
|
425
|
+
clientId: null,
|
|
426
|
+
oauthState: null
|
|
427
|
+
});
|
|
428
|
+
}
|
|
317
429
|
async listIds(userId) {
|
|
318
430
|
const sessions2 = await this.list(userId);
|
|
319
431
|
return sessions2.map((session) => session.sessionId);
|
|
@@ -333,7 +445,7 @@ var RedisStorageBackend = class {
|
|
|
333
445
|
if (staleSessionIds.length > 0) {
|
|
334
446
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
335
447
|
}
|
|
336
|
-
return results.filter((session) => session !== null);
|
|
448
|
+
return results.filter((session) => session !== null).map((session) => normalizeStoredSession(session));
|
|
337
449
|
} catch (error) {
|
|
338
450
|
console.error(`[RedisStorageBackend] Failed to get session data for ${userId}:`, error);
|
|
339
451
|
return [];
|
|
@@ -342,9 +454,10 @@ var RedisStorageBackend = class {
|
|
|
342
454
|
async delete(userId, sessionId) {
|
|
343
455
|
try {
|
|
344
456
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
457
|
+
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
345
458
|
const userIdKey = this.getUserIdKey(userId);
|
|
346
459
|
await this.redis.srem(userIdKey, sessionId);
|
|
347
|
-
await this.redis.del(sessionKey);
|
|
460
|
+
await this.redis.del(sessionKey, credentialsKey);
|
|
348
461
|
} catch (error) {
|
|
349
462
|
console.error("[RedisStorageBackend] Failed to remove session:", error);
|
|
350
463
|
}
|
|
@@ -359,7 +472,7 @@ var RedisStorageBackend = class {
|
|
|
359
472
|
return null;
|
|
360
473
|
}
|
|
361
474
|
try {
|
|
362
|
-
return JSON.parse(data).sessionId;
|
|
475
|
+
return normalizeStoredSession(JSON.parse(data)).sessionId;
|
|
363
476
|
} catch (error) {
|
|
364
477
|
console.error("[RedisStorageBackend] Failed to parse session while listing all session IDs:", error);
|
|
365
478
|
return null;
|
|
@@ -375,8 +488,9 @@ var RedisStorageBackend = class {
|
|
|
375
488
|
async clearAll() {
|
|
376
489
|
try {
|
|
377
490
|
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
491
|
+
const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
|
|
378
492
|
const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
|
|
379
|
-
const allKeys = [...keys, ...userIdKeys];
|
|
493
|
+
const allKeys = [...keys, ...credentialKeys, ...userIdKeys];
|
|
380
494
|
if (allKeys.length > 0) {
|
|
381
495
|
await this.redis.del(...allKeys);
|
|
382
496
|
}
|
|
@@ -400,6 +514,7 @@ var RedisStorageBackend = class {
|
|
|
400
514
|
const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
|
|
401
515
|
if (staleSessionIds.length > 0) {
|
|
402
516
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
517
|
+
await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
|
|
403
518
|
}
|
|
404
519
|
const remainingCount = await this.redis.scard(userIdKey);
|
|
405
520
|
if (remainingCount === 0) {
|
|
@@ -425,6 +540,7 @@ var MemoryStorageBackend = class {
|
|
|
425
540
|
constructor() {
|
|
426
541
|
// Map<userId:sessionId, Session>
|
|
427
542
|
__publicField(this, "sessions", /* @__PURE__ */ new Map());
|
|
543
|
+
__publicField(this, "credentials", /* @__PURE__ */ new Map());
|
|
428
544
|
// Map<userId, Set<sessionId>>
|
|
429
545
|
__publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
|
|
430
546
|
}
|
|
@@ -437,36 +553,55 @@ var MemoryStorageBackend = class {
|
|
|
437
553
|
generateSessionId() {
|
|
438
554
|
return generateSessionId();
|
|
439
555
|
}
|
|
440
|
-
async create(session
|
|
556
|
+
async create(session) {
|
|
441
557
|
const { sessionId, userId } = session;
|
|
442
558
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
443
559
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
444
560
|
if (this.sessions.has(sessionKey)) {
|
|
445
561
|
throw new Error(`Session ${sessionId} already exists`);
|
|
446
562
|
}
|
|
447
|
-
this.sessions.set(sessionKey, session);
|
|
563
|
+
this.sessions.set(sessionKey, normalizeNewSession(session));
|
|
448
564
|
if (!this.userIdSessions.has(userId)) {
|
|
449
565
|
this.userIdSessions.set(userId, /* @__PURE__ */ new Set());
|
|
450
566
|
}
|
|
451
567
|
this.userIdSessions.get(userId).add(sessionId);
|
|
452
568
|
}
|
|
453
|
-
async update(userId, sessionId, data
|
|
569
|
+
async update(userId, sessionId, data) {
|
|
454
570
|
if (!userId || !sessionId) throw new Error("userId and sessionId required");
|
|
455
571
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
456
572
|
const current = this.sessions.get(sessionKey);
|
|
457
573
|
if (!current) {
|
|
458
574
|
throw new Error(`Session ${sessionId} not found`);
|
|
459
575
|
}
|
|
460
|
-
const updated =
|
|
461
|
-
...current,
|
|
462
|
-
...data
|
|
463
|
-
};
|
|
576
|
+
const updated = mergeSessionUpdate(current, data);
|
|
464
577
|
this.sessions.set(sessionKey, updated);
|
|
465
578
|
}
|
|
579
|
+
async patchCredentials(userId, sessionId, data) {
|
|
580
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
581
|
+
if (!this.sessions.has(sessionKey)) {
|
|
582
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
583
|
+
}
|
|
584
|
+
const current = this.credentials.get(sessionKey) ?? { sessionId, userId };
|
|
585
|
+
this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
586
|
+
}
|
|
466
587
|
async get(userId, sessionId) {
|
|
467
588
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
468
589
|
return this.sessions.get(sessionKey) || null;
|
|
469
590
|
}
|
|
591
|
+
async getCredentials(userId, sessionId) {
|
|
592
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
593
|
+
if (!this.sessions.has(sessionKey)) return null;
|
|
594
|
+
return this.credentials.get(sessionKey) ?? { sessionId, userId };
|
|
595
|
+
}
|
|
596
|
+
async clearCredentials(userId, sessionId) {
|
|
597
|
+
await this.patchCredentials(userId, sessionId, {
|
|
598
|
+
clientInformation: null,
|
|
599
|
+
tokens: null,
|
|
600
|
+
codeVerifier: null,
|
|
601
|
+
clientId: null,
|
|
602
|
+
oauthState: null
|
|
603
|
+
});
|
|
604
|
+
}
|
|
470
605
|
async listIds(userId) {
|
|
471
606
|
const set = this.userIdSessions.get(userId);
|
|
472
607
|
return set ? Array.from(set) : [];
|
|
@@ -486,6 +621,7 @@ var MemoryStorageBackend = class {
|
|
|
486
621
|
async delete(userId, sessionId) {
|
|
487
622
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
488
623
|
this.sessions.delete(sessionKey);
|
|
624
|
+
this.credentials.delete(sessionKey);
|
|
489
625
|
const set = this.userIdSessions.get(userId);
|
|
490
626
|
if (set) {
|
|
491
627
|
set.delete(sessionId);
|
|
@@ -499,9 +635,22 @@ var MemoryStorageBackend = class {
|
|
|
499
635
|
}
|
|
500
636
|
async clearAll() {
|
|
501
637
|
this.sessions.clear();
|
|
638
|
+
this.credentials.clear();
|
|
502
639
|
this.userIdSessions.clear();
|
|
503
640
|
}
|
|
504
641
|
async cleanupExpired() {
|
|
642
|
+
for (const [key, session] of this.sessions.entries()) {
|
|
643
|
+
if (!isSessionExpired(session)) continue;
|
|
644
|
+
this.sessions.delete(key);
|
|
645
|
+
this.credentials.delete(key);
|
|
646
|
+
const set = this.userIdSessions.get(session.userId);
|
|
647
|
+
if (set) {
|
|
648
|
+
set.delete(session.sessionId);
|
|
649
|
+
if (set.size === 0) {
|
|
650
|
+
this.userIdSessions.delete(session.userId);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
505
654
|
}
|
|
506
655
|
async disconnect() {
|
|
507
656
|
}
|
|
@@ -516,6 +665,7 @@ var FileStorageBackend = class {
|
|
|
516
665
|
constructor(options = {}) {
|
|
517
666
|
__publicField(this, "filePath");
|
|
518
667
|
__publicField(this, "memoryCache", null);
|
|
668
|
+
__publicField(this, "credentialsCache", null);
|
|
519
669
|
__publicField(this, "initialized", false);
|
|
520
670
|
this.filePath = options.path || "./sessions.json";
|
|
521
671
|
}
|
|
@@ -530,14 +680,22 @@ var FileStorageBackend = class {
|
|
|
530
680
|
const data = await fs2.promises.readFile(this.filePath, "utf-8");
|
|
531
681
|
const json = JSON.parse(data);
|
|
532
682
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
683
|
+
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
684
|
+
if (Array.isArray(json.sessions)) {
|
|
685
|
+
json.sessions.forEach((s) => {
|
|
686
|
+
const session = normalizeStoredSession(s);
|
|
687
|
+
this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
if (Array.isArray(json.credentials)) {
|
|
691
|
+
json.credentials.forEach((c) => {
|
|
692
|
+
this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
|
|
536
693
|
});
|
|
537
694
|
}
|
|
538
695
|
} catch (error) {
|
|
539
696
|
if (error.code === "ENOENT") {
|
|
540
697
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
698
|
+
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
541
699
|
await this.flush();
|
|
542
700
|
} else {
|
|
543
701
|
console.error("[FileStorage] Failed to load sessions:", error);
|
|
@@ -551,9 +709,11 @@ var FileStorageBackend = class {
|
|
|
551
709
|
if (!this.initialized) await this.init();
|
|
552
710
|
}
|
|
553
711
|
async flush() {
|
|
554
|
-
if (!this.memoryCache) return;
|
|
555
|
-
|
|
556
|
-
|
|
712
|
+
if (!this.memoryCache || !this.credentialsCache) return;
|
|
713
|
+
await fs2.promises.writeFile(this.filePath, JSON.stringify({
|
|
714
|
+
sessions: Array.from(this.memoryCache.values()),
|
|
715
|
+
credentials: Array.from(this.credentialsCache.values())
|
|
716
|
+
}, null, 2), "utf-8");
|
|
557
717
|
}
|
|
558
718
|
getSessionKey(userId, sessionId) {
|
|
559
719
|
return `${userId}:${sessionId}`;
|
|
@@ -561,7 +721,7 @@ var FileStorageBackend = class {
|
|
|
561
721
|
generateSessionId() {
|
|
562
722
|
return generateSessionId();
|
|
563
723
|
}
|
|
564
|
-
async create(session
|
|
724
|
+
async create(session) {
|
|
565
725
|
await this.ensureInitialized();
|
|
566
726
|
const { sessionId, userId } = session;
|
|
567
727
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
@@ -569,10 +729,10 @@ var FileStorageBackend = class {
|
|
|
569
729
|
if (this.memoryCache.has(sessionKey)) {
|
|
570
730
|
throw new Error(`Session ${sessionId} already exists`);
|
|
571
731
|
}
|
|
572
|
-
this.memoryCache.set(sessionKey, session);
|
|
732
|
+
this.memoryCache.set(sessionKey, normalizeNewSession(session));
|
|
573
733
|
await this.flush();
|
|
574
734
|
}
|
|
575
|
-
async update(userId, sessionId, data
|
|
735
|
+
async update(userId, sessionId, data) {
|
|
576
736
|
await this.ensureInitialized();
|
|
577
737
|
if (!userId || !sessionId) throw new Error("userId and sessionId required");
|
|
578
738
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
@@ -580,18 +740,40 @@ var FileStorageBackend = class {
|
|
|
580
740
|
if (!current) {
|
|
581
741
|
throw new Error(`Session ${sessionId} not found`);
|
|
582
742
|
}
|
|
583
|
-
const updated =
|
|
584
|
-
...current,
|
|
585
|
-
...data
|
|
586
|
-
};
|
|
743
|
+
const updated = mergeSessionUpdate(current, data);
|
|
587
744
|
this.memoryCache.set(sessionKey, updated);
|
|
588
745
|
await this.flush();
|
|
589
746
|
}
|
|
747
|
+
async patchCredentials(userId, sessionId, data) {
|
|
748
|
+
await this.ensureInitialized();
|
|
749
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
750
|
+
if (!this.memoryCache.has(sessionKey)) {
|
|
751
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
752
|
+
}
|
|
753
|
+
const current = this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
|
|
754
|
+
this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
755
|
+
await this.flush();
|
|
756
|
+
}
|
|
590
757
|
async get(userId, sessionId) {
|
|
591
758
|
await this.ensureInitialized();
|
|
592
759
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
593
760
|
return this.memoryCache.get(sessionKey) || null;
|
|
594
761
|
}
|
|
762
|
+
async getCredentials(userId, sessionId) {
|
|
763
|
+
await this.ensureInitialized();
|
|
764
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
765
|
+
if (!this.memoryCache.has(sessionKey)) return null;
|
|
766
|
+
return this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
|
|
767
|
+
}
|
|
768
|
+
async clearCredentials(userId, sessionId) {
|
|
769
|
+
await this.patchCredentials(userId, sessionId, {
|
|
770
|
+
clientInformation: null,
|
|
771
|
+
tokens: null,
|
|
772
|
+
codeVerifier: null,
|
|
773
|
+
clientId: null,
|
|
774
|
+
oauthState: null
|
|
775
|
+
});
|
|
776
|
+
}
|
|
595
777
|
async list(userId) {
|
|
596
778
|
await this.ensureInitialized();
|
|
597
779
|
return Array.from(this.memoryCache.values()).filter((s) => s.userId === userId);
|
|
@@ -603,7 +785,9 @@ var FileStorageBackend = class {
|
|
|
603
785
|
async delete(userId, sessionId) {
|
|
604
786
|
await this.ensureInitialized();
|
|
605
787
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
606
|
-
|
|
788
|
+
const deleted = this.memoryCache.delete(sessionKey);
|
|
789
|
+
this.credentialsCache.delete(sessionKey);
|
|
790
|
+
if (deleted) {
|
|
607
791
|
await this.flush();
|
|
608
792
|
}
|
|
609
793
|
}
|
|
@@ -614,10 +798,21 @@ var FileStorageBackend = class {
|
|
|
614
798
|
async clearAll() {
|
|
615
799
|
await this.ensureInitialized();
|
|
616
800
|
this.memoryCache.clear();
|
|
801
|
+
this.credentialsCache.clear();
|
|
617
802
|
await this.flush();
|
|
618
803
|
}
|
|
619
804
|
async cleanupExpired() {
|
|
620
805
|
await this.ensureInitialized();
|
|
806
|
+
let changed = false;
|
|
807
|
+
for (const [key, session] of this.memoryCache.entries()) {
|
|
808
|
+
if (!isSessionExpired(session)) continue;
|
|
809
|
+
this.memoryCache.delete(key);
|
|
810
|
+
this.credentialsCache.delete(key);
|
|
811
|
+
changed = true;
|
|
812
|
+
}
|
|
813
|
+
if (changed) {
|
|
814
|
+
await this.flush();
|
|
815
|
+
}
|
|
621
816
|
}
|
|
622
817
|
async disconnect() {
|
|
623
818
|
}
|
|
@@ -629,10 +824,12 @@ var SqliteStorage = class {
|
|
|
629
824
|
constructor(options = {}) {
|
|
630
825
|
__publicField(this, "db", null);
|
|
631
826
|
__publicField(this, "table");
|
|
827
|
+
__publicField(this, "credentialsTable");
|
|
632
828
|
__publicField(this, "initialized", false);
|
|
633
829
|
__publicField(this, "dbPath");
|
|
634
830
|
this.dbPath = options.path || "./sessions.db";
|
|
635
831
|
this.table = options.table || "mcp_sessions";
|
|
832
|
+
this.credentialsTable = `${this.table}_credentials`;
|
|
636
833
|
}
|
|
637
834
|
async init() {
|
|
638
835
|
if (this.initialized) return;
|
|
@@ -643,6 +840,7 @@ var SqliteStorage = class {
|
|
|
643
840
|
fs2__namespace.mkdirSync(dir, { recursive: true });
|
|
644
841
|
}
|
|
645
842
|
this.db = new DatabaseConstructor(this.dbPath);
|
|
843
|
+
this.db.pragma("foreign_keys = ON");
|
|
646
844
|
this.db.exec(`
|
|
647
845
|
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
648
846
|
sessionId TEXT PRIMARY KEY,
|
|
@@ -651,6 +849,13 @@ var SqliteStorage = class {
|
|
|
651
849
|
expiresAt INTEGER
|
|
652
850
|
);
|
|
653
851
|
CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
|
|
852
|
+
CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
|
|
853
|
+
sessionId TEXT NOT NULL,
|
|
854
|
+
userId TEXT NOT NULL,
|
|
855
|
+
data TEXT NOT NULL,
|
|
856
|
+
PRIMARY KEY (userId, sessionId),
|
|
857
|
+
FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
|
|
858
|
+
);
|
|
654
859
|
`);
|
|
655
860
|
this.initialized = true;
|
|
656
861
|
console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
|
|
@@ -671,18 +876,18 @@ var SqliteStorage = class {
|
|
|
671
876
|
generateSessionId() {
|
|
672
877
|
return generateSessionId();
|
|
673
878
|
}
|
|
674
|
-
async create(session
|
|
879
|
+
async create(session) {
|
|
675
880
|
this.ensureInitialized();
|
|
676
881
|
const { sessionId, userId } = session;
|
|
677
882
|
if (!sessionId || !userId) {
|
|
678
883
|
throw new Error("userId and sessionId required");
|
|
679
884
|
}
|
|
680
|
-
const
|
|
885
|
+
const sessionWithLifecycle = normalizeNewSession(session);
|
|
681
886
|
try {
|
|
682
887
|
const stmt = this.db.prepare(
|
|
683
888
|
`INSERT INTO ${this.table} (sessionId, userId, data, expiresAt) VALUES (?, ?, ?, ?)`
|
|
684
889
|
);
|
|
685
|
-
stmt.run(sessionId, userId, JSON.stringify(
|
|
890
|
+
stmt.run(sessionId, userId, JSON.stringify(sessionWithLifecycle), sessionWithLifecycle.expiresAt ?? null);
|
|
686
891
|
} catch (error) {
|
|
687
892
|
if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
688
893
|
throw new Error(`Session ${sessionId} already exists`);
|
|
@@ -690,7 +895,7 @@ var SqliteStorage = class {
|
|
|
690
895
|
throw error;
|
|
691
896
|
}
|
|
692
897
|
}
|
|
693
|
-
async update(userId, sessionId, data
|
|
898
|
+
async update(userId, sessionId, data) {
|
|
694
899
|
this.ensureInitialized();
|
|
695
900
|
if (!sessionId || !userId) {
|
|
696
901
|
throw new Error("userId and sessionId required");
|
|
@@ -699,12 +904,25 @@ var SqliteStorage = class {
|
|
|
699
904
|
if (!currentSession) {
|
|
700
905
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
701
906
|
}
|
|
702
|
-
const updatedSession =
|
|
703
|
-
const expiresAt = ttl ? Date.now() + ttl * 1e3 : null;
|
|
907
|
+
const updatedSession = mergeSessionUpdate(currentSession, data);
|
|
704
908
|
const stmt = this.db.prepare(
|
|
705
909
|
`UPDATE ${this.table} SET data = ?, expiresAt = ? WHERE sessionId = ? AND userId = ?`
|
|
706
910
|
);
|
|
707
|
-
stmt.run(JSON.stringify(updatedSession), expiresAt, sessionId, userId);
|
|
911
|
+
stmt.run(JSON.stringify(updatedSession), updatedSession.expiresAt ?? null, sessionId, userId);
|
|
912
|
+
}
|
|
913
|
+
async patchCredentials(userId, sessionId, data) {
|
|
914
|
+
this.ensureInitialized();
|
|
915
|
+
if (!await this.get(userId, sessionId)) {
|
|
916
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
917
|
+
}
|
|
918
|
+
const current = await this.getCredentials(userId, sessionId) ?? { sessionId, userId };
|
|
919
|
+
const credentials = { ...current, ...data, sessionId, userId };
|
|
920
|
+
const stmt = this.db.prepare(
|
|
921
|
+
`INSERT INTO ${this.credentialsTable} (sessionId, userId, data)
|
|
922
|
+
VALUES (?, ?, ?)
|
|
923
|
+
ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
|
|
924
|
+
);
|
|
925
|
+
stmt.run(sessionId, userId, JSON.stringify(credentials));
|
|
708
926
|
}
|
|
709
927
|
async get(userId, sessionId) {
|
|
710
928
|
this.ensureInitialized();
|
|
@@ -713,7 +931,25 @@ var SqliteStorage = class {
|
|
|
713
931
|
);
|
|
714
932
|
const row = stmt.get(sessionId, userId);
|
|
715
933
|
if (!row) return null;
|
|
716
|
-
return JSON.parse(row.data);
|
|
934
|
+
return normalizeStoredSession(JSON.parse(row.data));
|
|
935
|
+
}
|
|
936
|
+
async getCredentials(userId, sessionId) {
|
|
937
|
+
this.ensureInitialized();
|
|
938
|
+
if (!await this.get(userId, sessionId)) return null;
|
|
939
|
+
const stmt = this.db.prepare(
|
|
940
|
+
`SELECT data FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
941
|
+
);
|
|
942
|
+
const row = stmt.get(sessionId, userId);
|
|
943
|
+
return row ? JSON.parse(row.data) : { sessionId, userId };
|
|
944
|
+
}
|
|
945
|
+
async clearCredentials(userId, sessionId) {
|
|
946
|
+
await this.patchCredentials(userId, sessionId, {
|
|
947
|
+
clientInformation: null,
|
|
948
|
+
tokens: null,
|
|
949
|
+
codeVerifier: null,
|
|
950
|
+
clientId: null,
|
|
951
|
+
oauthState: null
|
|
952
|
+
});
|
|
717
953
|
}
|
|
718
954
|
async list(userId) {
|
|
719
955
|
this.ensureInitialized();
|
|
@@ -721,7 +957,7 @@ var SqliteStorage = class {
|
|
|
721
957
|
`SELECT data FROM ${this.table} WHERE userId = ?`
|
|
722
958
|
);
|
|
723
959
|
const rows = stmt.all(userId);
|
|
724
|
-
return rows.map((row) => JSON.parse(row.data));
|
|
960
|
+
return rows.map((row) => normalizeStoredSession(JSON.parse(row.data)));
|
|
725
961
|
}
|
|
726
962
|
async listIds(userId) {
|
|
727
963
|
this.ensureInitialized();
|
|
@@ -736,6 +972,9 @@ var SqliteStorage = class {
|
|
|
736
972
|
const stmt = this.db.prepare(
|
|
737
973
|
`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
738
974
|
);
|
|
975
|
+
this.db.prepare(
|
|
976
|
+
`DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
977
|
+
).run(sessionId, userId);
|
|
739
978
|
stmt.run(sessionId, userId);
|
|
740
979
|
}
|
|
741
980
|
async listAllIds() {
|
|
@@ -746,16 +985,28 @@ var SqliteStorage = class {
|
|
|
746
985
|
}
|
|
747
986
|
async clearAll() {
|
|
748
987
|
this.ensureInitialized();
|
|
988
|
+
this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
|
|
749
989
|
const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
|
|
750
990
|
stmt.run();
|
|
751
991
|
}
|
|
752
992
|
async cleanupExpired() {
|
|
753
993
|
this.ensureInitialized();
|
|
754
|
-
const
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
994
|
+
const rows = this.db.prepare(`SELECT sessionId, userId, data FROM ${this.table}`).all();
|
|
995
|
+
const deleteStmt = this.db.prepare(`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`);
|
|
996
|
+
for (const row of rows) {
|
|
997
|
+
const session = normalizeStoredSession(JSON.parse(row.data));
|
|
998
|
+
if (isSessionExpired(session)) {
|
|
999
|
+
deleteStmt.run(row.sessionId, row.userId);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
this.db.prepare(
|
|
1003
|
+
`DELETE FROM ${this.credentialsTable}
|
|
1004
|
+
WHERE NOT EXISTS (
|
|
1005
|
+
SELECT 1 FROM ${this.table}
|
|
1006
|
+
WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
|
|
1007
|
+
AND ${this.table}.userId = ${this.credentialsTable}.userId
|
|
1008
|
+
)`
|
|
1009
|
+
).run();
|
|
759
1010
|
}
|
|
760
1011
|
async disconnect() {
|
|
761
1012
|
if (this.db) {
|
|
@@ -840,19 +1091,21 @@ function decryptObject(data) {
|
|
|
840
1091
|
var SupabaseStorageBackend = class {
|
|
841
1092
|
constructor(supabase) {
|
|
842
1093
|
this.supabase = supabase;
|
|
843
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
844
1094
|
}
|
|
845
1095
|
async init() {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1096
|
+
await this.assertTable("mcp_sessions", "session_id");
|
|
1097
|
+
await this.assertTable("mcp_credentials", "session_id");
|
|
1098
|
+
console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
|
|
1099
|
+
}
|
|
1100
|
+
async assertTable(table, column) {
|
|
1101
|
+
const { error } = await this.supabase.from(table).select(column).limit(0);
|
|
1102
|
+
if (!error) return;
|
|
1103
|
+
if (error.code === "42P01") {
|
|
1104
|
+
throw new Error(
|
|
1105
|
+
`[SupabaseStorage] Table "${table}" not found in your database. Please run "npx mcp-ts supabase-init" to set up the required storage schema.`
|
|
1106
|
+
);
|
|
854
1107
|
}
|
|
855
|
-
|
|
1108
|
+
throw new Error(`[SupabaseStorage] Initialization check failed for "${table}": ${error.message}`);
|
|
856
1109
|
}
|
|
857
1110
|
generateSessionId() {
|
|
858
1111
|
return generateSessionId();
|
|
@@ -866,37 +1119,49 @@ var SupabaseStorageBackend = class {
|
|
|
866
1119
|
transportType: row.transport_type,
|
|
867
1120
|
callbackUrl: row.callback_url,
|
|
868
1121
|
createdAt: new Date(row.created_at).getTime(),
|
|
1122
|
+
updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
|
|
1123
|
+
expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
|
|
869
1124
|
userId: row.user_id,
|
|
870
1125
|
headers: decryptObject(row.headers),
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1126
|
+
authUrl: row.auth_url,
|
|
1127
|
+
status: row.status ?? "pending"
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
mapRowToCredentials(row, userId, sessionId) {
|
|
1131
|
+
return {
|
|
1132
|
+
sessionId,
|
|
1133
|
+
userId,
|
|
1134
|
+
clientInformation: decryptObject(row?.client_information),
|
|
1135
|
+
tokens: decryptObject(row?.tokens),
|
|
1136
|
+
codeVerifier: decryptObject(row?.code_verifier),
|
|
1137
|
+
clientId: row?.client_id,
|
|
1138
|
+
oauthState: row?.oauth_state
|
|
876
1139
|
};
|
|
877
1140
|
}
|
|
878
|
-
|
|
1141
|
+
hasCredentialData(data) {
|
|
1142
|
+
return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
|
|
1143
|
+
}
|
|
1144
|
+
async create(session) {
|
|
879
1145
|
const { sessionId, userId } = session;
|
|
880
1146
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
881
|
-
const
|
|
882
|
-
const
|
|
1147
|
+
const status = session.status ?? "pending";
|
|
1148
|
+
const createdAt = new Date(session.createdAt || Date.now()).toISOString();
|
|
1149
|
+
const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
|
|
1150
|
+
const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
|
|
883
1151
|
const { error } = await this.supabase.from("mcp_sessions").insert({
|
|
884
1152
|
session_id: sessionId,
|
|
885
1153
|
user_id: userId,
|
|
886
|
-
// Maps user_id to userId to support RLS using auth.uid()
|
|
887
1154
|
server_id: session.serverId,
|
|
888
1155
|
server_name: session.serverName,
|
|
889
1156
|
server_url: session.serverUrl,
|
|
890
1157
|
transport_type: session.transportType,
|
|
891
1158
|
callback_url: session.callbackUrl,
|
|
892
|
-
created_at:
|
|
1159
|
+
created_at: createdAt,
|
|
1160
|
+
updated_at: updatedAt,
|
|
893
1161
|
headers: encryptObject(session.headers),
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
code_verifier: session.codeVerifier,
|
|
898
|
-
client_id: session.clientId,
|
|
899
|
-
expires_at: expiresAt
|
|
1162
|
+
auth_url: session.authUrl ?? null,
|
|
1163
|
+
status,
|
|
1164
|
+
expires_at: expiresAt === null ? null : new Date(expiresAt).toISOString()
|
|
900
1165
|
});
|
|
901
1166
|
if (error) {
|
|
902
1167
|
if (error.code === "23505") {
|
|
@@ -905,11 +1170,8 @@ var SupabaseStorageBackend = class {
|
|
|
905
1170
|
throw new Error(`Failed to create session in Supabase: ${error.message}`);
|
|
906
1171
|
}
|
|
907
1172
|
}
|
|
908
|
-
async update(userId, sessionId, data
|
|
909
|
-
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
910
|
-
const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
|
|
1173
|
+
async update(userId, sessionId, data) {
|
|
911
1174
|
const updateData = {
|
|
912
|
-
expires_at: expiresAt,
|
|
913
1175
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
914
1176
|
};
|
|
915
1177
|
if ("serverId" in data) updateData.server_id = data.serverId;
|
|
@@ -917,20 +1179,50 @@ var SupabaseStorageBackend = class {
|
|
|
917
1179
|
if ("serverUrl" in data) updateData.server_url = data.serverUrl;
|
|
918
1180
|
if ("transportType" in data) updateData.transport_type = data.transportType;
|
|
919
1181
|
if ("callbackUrl" in data) updateData.callback_url = data.callbackUrl;
|
|
920
|
-
if ("
|
|
1182
|
+
if ("status" in data) {
|
|
1183
|
+
const status = data.status ?? "pending";
|
|
1184
|
+
const expiresAt = resolveSessionExpiresAt(status);
|
|
1185
|
+
updateData.status = status;
|
|
1186
|
+
updateData.expires_at = expiresAt === null ? null : new Date(expiresAt).toISOString();
|
|
1187
|
+
}
|
|
921
1188
|
if ("headers" in data) updateData.headers = encryptObject(data.headers);
|
|
922
|
-
if ("
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
if (
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1189
|
+
if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
|
|
1190
|
+
const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
|
|
1191
|
+
let updatedRows = null;
|
|
1192
|
+
if (shouldUpdateSession) {
|
|
1193
|
+
const result = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId).select("id");
|
|
1194
|
+
if (result.error) {
|
|
1195
|
+
throw new Error(`Failed to update session: ${result.error.message}`);
|
|
1196
|
+
}
|
|
1197
|
+
updatedRows = result.data;
|
|
1198
|
+
} else {
|
|
1199
|
+
const result = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
|
|
1200
|
+
if (result.error) {
|
|
1201
|
+
throw new Error(`Failed to update session: ${result.error.message}`);
|
|
1202
|
+
}
|
|
1203
|
+
updatedRows = result.data;
|
|
929
1204
|
}
|
|
930
1205
|
if (!updatedRows || updatedRows.length === 0) {
|
|
931
1206
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
932
1207
|
}
|
|
933
1208
|
}
|
|
1209
|
+
async patchCredentials(userId, sessionId, data) {
|
|
1210
|
+
if (!this.hasCredentialData(data)) return;
|
|
1211
|
+
const row = {
|
|
1212
|
+
user_id: userId,
|
|
1213
|
+
session_id: sessionId,
|
|
1214
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1215
|
+
};
|
|
1216
|
+
if ("clientInformation" in data) row.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
|
|
1217
|
+
if ("tokens" in data) row.tokens = data.tokens == null ? null : encryptObject(data.tokens);
|
|
1218
|
+
if ("codeVerifier" in data) row.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
|
|
1219
|
+
if ("clientId" in data) row.client_id = data.clientId ?? null;
|
|
1220
|
+
if ("oauthState" in data) row.oauth_state = data.oauthState ?? null;
|
|
1221
|
+
const { error } = await this.supabase.from("mcp_credentials").upsert(row, { onConflict: "user_id,session_id" });
|
|
1222
|
+
if (error) {
|
|
1223
|
+
throw new Error(`Failed to update credentials: ${error.message}`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
934
1226
|
async get(userId, sessionId) {
|
|
935
1227
|
const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
936
1228
|
if (error) {
|
|
@@ -940,6 +1232,21 @@ var SupabaseStorageBackend = class {
|
|
|
940
1232
|
if (!data) return null;
|
|
941
1233
|
return this.mapRowToSessionData(data);
|
|
942
1234
|
}
|
|
1235
|
+
async getCredentials(userId, sessionId) {
|
|
1236
|
+
const { data, error } = await this.supabase.from("mcp_credentials").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
1237
|
+
if (error) {
|
|
1238
|
+
console.error("[SupabaseStorage] Failed to get credentials:", error);
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
if (data) {
|
|
1242
|
+
return this.mapRowToCredentials(data, userId, sessionId);
|
|
1243
|
+
}
|
|
1244
|
+
const { data: sessionRows, error: sessionError } = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
|
|
1245
|
+
if (sessionError || !sessionRows || sessionRows.length === 0) {
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
return { sessionId, userId };
|
|
1249
|
+
}
|
|
943
1250
|
async list(userId) {
|
|
944
1251
|
const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
|
|
945
1252
|
if (error) {
|
|
@@ -948,6 +1255,12 @@ var SupabaseStorageBackend = class {
|
|
|
948
1255
|
}
|
|
949
1256
|
return data.map((row) => this.mapRowToSessionData(row));
|
|
950
1257
|
}
|
|
1258
|
+
async clearCredentials(userId, sessionId) {
|
|
1259
|
+
const { error } = await this.supabase.from("mcp_credentials").delete().eq("user_id", userId).eq("session_id", sessionId);
|
|
1260
|
+
if (error) {
|
|
1261
|
+
throw new Error(`Failed to clear credentials: ${error.message}`);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
951
1264
|
async delete(userId, sessionId) {
|
|
952
1265
|
const { error } = await this.supabase.from("mcp_sessions").delete().eq("user_id", userId).eq("session_id", sessionId);
|
|
953
1266
|
if (error) {
|
|
@@ -971,15 +1284,24 @@ var SupabaseStorageBackend = class {
|
|
|
971
1284
|
return data.map((row) => row.session_id);
|
|
972
1285
|
}
|
|
973
1286
|
async clearAll() {
|
|
1287
|
+
const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
|
|
1288
|
+
if (credentialsError) {
|
|
1289
|
+
console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
|
|
1290
|
+
}
|
|
974
1291
|
const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
|
|
975
1292
|
if (error) {
|
|
976
1293
|
console.error("[SupabaseStorage] Failed to clear sessions:", error);
|
|
977
1294
|
}
|
|
978
1295
|
}
|
|
979
1296
|
async cleanupExpired() {
|
|
980
|
-
const { error } = await this.supabase.from("mcp_sessions").delete().lt("expires_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
981
|
-
if (
|
|
982
|
-
console.error("[SupabaseStorage] Failed to cleanup expired sessions:",
|
|
1297
|
+
const { error: transientError } = await this.supabase.from("mcp_sessions").delete().not("expires_at", "is", null).neq("status", "active").lt("expires_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
1298
|
+
if (transientError) {
|
|
1299
|
+
console.error("[SupabaseStorage] Failed to cleanup expired inactive sessions:", transientError);
|
|
1300
|
+
}
|
|
1301
|
+
const dormantCutoff = new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString();
|
|
1302
|
+
const { error: dormantError } = await this.supabase.from("mcp_sessions").delete().eq("status", "active").lt("updated_at", dormantCutoff);
|
|
1303
|
+
if (dormantError) {
|
|
1304
|
+
console.error("[SupabaseStorage] Failed to cleanup dormant active sessions:", dormantError);
|
|
983
1305
|
}
|
|
984
1306
|
}
|
|
985
1307
|
async disconnect() {
|
|
@@ -991,23 +1313,29 @@ init_cjs_shims();
|
|
|
991
1313
|
var NeonStorageBackend = class {
|
|
992
1314
|
constructor(sql, options = {}) {
|
|
993
1315
|
this.sql = sql;
|
|
994
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
995
1316
|
__publicField(this, "tableName");
|
|
1317
|
+
__publicField(this, "credentialsTableName");
|
|
996
1318
|
const schema = options.schema || "public";
|
|
997
1319
|
const table = options.table || "mcp_sessions";
|
|
1320
|
+
const credentialsTable = options.credentialsTable || "mcp_credentials";
|
|
998
1321
|
this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
|
|
1322
|
+
this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
|
|
999
1323
|
}
|
|
1000
1324
|
async init() {
|
|
1325
|
+
await this.assertTable(this.tableName, "mcp_sessions");
|
|
1326
|
+
await this.assertTable(this.credentialsTableName, "mcp_credentials");
|
|
1327
|
+
console.log("[mcp-ts][Storage] Neon: storage tables verified.");
|
|
1328
|
+
}
|
|
1329
|
+
async assertTable(qualifiedName, displayName) {
|
|
1001
1330
|
const [{ exists } = { exists: null }] = await this.sql.query(
|
|
1002
1331
|
"SELECT to_regclass($1) AS exists",
|
|
1003
|
-
[
|
|
1332
|
+
[qualifiedName.replace(/"/g, "")]
|
|
1004
1333
|
);
|
|
1005
1334
|
if (!exists) {
|
|
1006
1335
|
throw new Error(
|
|
1007
|
-
|
|
1336
|
+
`[NeonStorage] Table "${displayName}" not found in your database. Please create it using the Neon storage guide in docs/storage-backends/neon.md.`
|
|
1008
1337
|
);
|
|
1009
1338
|
}
|
|
1010
|
-
console.log('[mcp-ts][Storage] Neon: "mcp_sessions" table verified.');
|
|
1011
1339
|
}
|
|
1012
1340
|
generateSessionId() {
|
|
1013
1341
|
return generateSessionId();
|
|
@@ -1027,20 +1355,35 @@ var NeonStorageBackend = class {
|
|
|
1027
1355
|
transportType: row.transport_type,
|
|
1028
1356
|
callbackUrl: row.callback_url,
|
|
1029
1357
|
createdAt: new Date(row.created_at).getTime(),
|
|
1358
|
+
updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
|
|
1359
|
+
expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
|
|
1030
1360
|
userId: row.user_id,
|
|
1031
1361
|
headers: decryptObject(row.headers),
|
|
1032
|
-
|
|
1033
|
-
|
|
1362
|
+
authUrl: row.auth_url ?? void 0,
|
|
1363
|
+
status: row.status ?? "pending"
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
mapRowToCredentials(row, userId, sessionId) {
|
|
1367
|
+
return {
|
|
1368
|
+
sessionId,
|
|
1369
|
+
userId,
|
|
1370
|
+
clientInformation: decryptObject(row.client_information),
|
|
1034
1371
|
tokens: decryptObject(row.tokens),
|
|
1035
|
-
codeVerifier: row.code_verifier
|
|
1036
|
-
clientId: row.client_id ?? void 0
|
|
1372
|
+
codeVerifier: decryptObject(row.code_verifier),
|
|
1373
|
+
clientId: row.client_id ?? void 0,
|
|
1374
|
+
oauthState: row.oauth_state
|
|
1037
1375
|
};
|
|
1038
1376
|
}
|
|
1039
|
-
|
|
1377
|
+
hasCredentialData(data) {
|
|
1378
|
+
return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
|
|
1379
|
+
}
|
|
1380
|
+
async create(session) {
|
|
1040
1381
|
const { sessionId, userId } = session;
|
|
1041
1382
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
1042
|
-
const
|
|
1043
|
-
const
|
|
1383
|
+
const status = session.status ?? "pending";
|
|
1384
|
+
const createdAt = new Date(session.createdAt || Date.now()).toISOString();
|
|
1385
|
+
const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
|
|
1386
|
+
const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
|
|
1044
1387
|
try {
|
|
1045
1388
|
await this.sql.query(
|
|
1046
1389
|
`INSERT INTO ${this.tableName} (
|
|
@@ -1052,16 +1395,14 @@ var NeonStorageBackend = class {
|
|
|
1052
1395
|
transport_type,
|
|
1053
1396
|
callback_url,
|
|
1054
1397
|
created_at,
|
|
1398
|
+
updated_at,
|
|
1055
1399
|
headers,
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
tokens,
|
|
1059
|
-
code_verifier,
|
|
1060
|
-
client_id,
|
|
1400
|
+
auth_url,
|
|
1401
|
+
status,
|
|
1061
1402
|
expires_at
|
|
1062
1403
|
) VALUES (
|
|
1063
1404
|
$1, $2, $3, $4, $5, $6, $7, $8,
|
|
1064
|
-
$9, $10, $11, $12, $13
|
|
1405
|
+
$9, $10, $11, $12, $13
|
|
1065
1406
|
)`,
|
|
1066
1407
|
[
|
|
1067
1408
|
sessionId,
|
|
@@ -1071,14 +1412,12 @@ var NeonStorageBackend = class {
|
|
|
1071
1412
|
session.serverUrl,
|
|
1072
1413
|
session.transportType,
|
|
1073
1414
|
session.callbackUrl,
|
|
1074
|
-
|
|
1415
|
+
createdAt,
|
|
1416
|
+
updatedAt,
|
|
1075
1417
|
encryptObject(session.headers),
|
|
1076
|
-
session.
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
session.codeVerifier,
|
|
1080
|
-
session.clientId,
|
|
1081
|
-
expiresAt
|
|
1418
|
+
session.authUrl ?? null,
|
|
1419
|
+
status,
|
|
1420
|
+
expiresAt === null ? null : new Date(expiresAt).toISOString()
|
|
1082
1421
|
]
|
|
1083
1422
|
);
|
|
1084
1423
|
} catch (error) {
|
|
@@ -1088,52 +1427,86 @@ var NeonStorageBackend = class {
|
|
|
1088
1427
|
throw new Error(`Failed to create session in Neon: ${error.message}`);
|
|
1089
1428
|
}
|
|
1090
1429
|
}
|
|
1091
|
-
async update(userId, sessionId, data
|
|
1430
|
+
async update(userId, sessionId, data) {
|
|
1092
1431
|
const currentSession = await this.get(userId, sessionId);
|
|
1093
1432
|
if (!currentSession) {
|
|
1094
1433
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1095
1434
|
}
|
|
1096
1435
|
const updatedSession = { ...currentSession, ...data };
|
|
1097
|
-
const
|
|
1098
|
-
const expiresAt =
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1436
|
+
const status = updatedSession.status ?? "pending";
|
|
1437
|
+
const expiresAt = resolveSessionExpiresAt(status);
|
|
1438
|
+
const shouldUpdateSession = "serverId" in data || "serverName" in data || "serverUrl" in data || "transportType" in data || "callbackUrl" in data || "status" in data || "headers" in data || "authUrl" in data;
|
|
1439
|
+
if (shouldUpdateSession) {
|
|
1440
|
+
const updatedRows = await this.sql.query(
|
|
1441
|
+
`UPDATE ${this.tableName}
|
|
1442
|
+
SET
|
|
1443
|
+
server_id = $1,
|
|
1444
|
+
server_name = $2,
|
|
1445
|
+
server_url = $3,
|
|
1446
|
+
transport_type = $4,
|
|
1447
|
+
callback_url = $5,
|
|
1448
|
+
status = $6,
|
|
1449
|
+
headers = $7,
|
|
1450
|
+
auth_url = $8,
|
|
1451
|
+
expires_at = $9,
|
|
1452
|
+
updated_at = now()
|
|
1453
|
+
WHERE user_id = $10 AND session_id = $11
|
|
1454
|
+
RETURNING id`,
|
|
1455
|
+
[
|
|
1456
|
+
updatedSession.serverId,
|
|
1457
|
+
updatedSession.serverName,
|
|
1458
|
+
updatedSession.serverUrl,
|
|
1459
|
+
updatedSession.transportType,
|
|
1460
|
+
updatedSession.callbackUrl,
|
|
1461
|
+
status,
|
|
1462
|
+
encryptObject(updatedSession.headers),
|
|
1463
|
+
updatedSession.authUrl ?? null,
|
|
1464
|
+
expiresAt === null ? null : new Date(expiresAt).toISOString(),
|
|
1465
|
+
userId,
|
|
1466
|
+
sessionId
|
|
1467
|
+
]
|
|
1468
|
+
);
|
|
1469
|
+
if (updatedRows.length === 0) {
|
|
1470
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
async patchCredentials(userId, sessionId, data) {
|
|
1475
|
+
if (!this.hasCredentialData(data)) return;
|
|
1476
|
+
await this.sql.query(
|
|
1477
|
+
`INSERT INTO ${this.credentialsTableName} (
|
|
1478
|
+
user_id,
|
|
1479
|
+
session_id,
|
|
1480
|
+
client_information,
|
|
1481
|
+
tokens,
|
|
1482
|
+
code_verifier,
|
|
1483
|
+
client_id,
|
|
1484
|
+
oauth_state,
|
|
1485
|
+
updated_at
|
|
1486
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, now())
|
|
1487
|
+
ON CONFLICT (user_id, session_id)
|
|
1488
|
+
DO UPDATE SET
|
|
1489
|
+
client_information = CASE WHEN $8 THEN EXCLUDED.client_information ELSE ${this.credentialsTableName}.client_information END,
|
|
1490
|
+
tokens = CASE WHEN $9 THEN EXCLUDED.tokens ELSE ${this.credentialsTableName}.tokens END,
|
|
1491
|
+
code_verifier = CASE WHEN $10 THEN EXCLUDED.code_verifier ELSE ${this.credentialsTableName}.code_verifier END,
|
|
1492
|
+
client_id = CASE WHEN $11 THEN EXCLUDED.client_id ELSE ${this.credentialsTableName}.client_id END,
|
|
1493
|
+
oauth_state = CASE WHEN $12 THEN EXCLUDED.oauth_state ELSE ${this.credentialsTableName}.oauth_state END,
|
|
1494
|
+
updated_at = now()`,
|
|
1117
1495
|
[
|
|
1118
|
-
updatedSession.serverId,
|
|
1119
|
-
updatedSession.serverName,
|
|
1120
|
-
updatedSession.serverUrl,
|
|
1121
|
-
updatedSession.transportType,
|
|
1122
|
-
updatedSession.callbackUrl,
|
|
1123
|
-
updatedSession.active ?? false,
|
|
1124
|
-
encryptObject(updatedSession.headers),
|
|
1125
|
-
updatedSession.clientInformation,
|
|
1126
|
-
encryptObject(updatedSession.tokens),
|
|
1127
|
-
updatedSession.codeVerifier,
|
|
1128
|
-
updatedSession.clientId,
|
|
1129
|
-
expiresAt,
|
|
1130
1496
|
userId,
|
|
1131
|
-
sessionId
|
|
1497
|
+
sessionId,
|
|
1498
|
+
"clientInformation" in data ? data.clientInformation == null ? null : encryptObject(data.clientInformation) : null,
|
|
1499
|
+
"tokens" in data ? data.tokens == null ? null : encryptObject(data.tokens) : null,
|
|
1500
|
+
"codeVerifier" in data ? data.codeVerifier == null ? null : encryptObject(data.codeVerifier) : null,
|
|
1501
|
+
"clientId" in data ? data.clientId ?? null : null,
|
|
1502
|
+
"oauthState" in data ? data.oauthState ?? null : null,
|
|
1503
|
+
"clientInformation" in data,
|
|
1504
|
+
"tokens" in data,
|
|
1505
|
+
"codeVerifier" in data,
|
|
1506
|
+
"clientId" in data,
|
|
1507
|
+
"oauthState" in data
|
|
1132
1508
|
]
|
|
1133
1509
|
);
|
|
1134
|
-
if (updatedRows.length === 0) {
|
|
1135
|
-
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1136
|
-
}
|
|
1137
1510
|
}
|
|
1138
1511
|
async get(userId, sessionId) {
|
|
1139
1512
|
try {
|
|
@@ -1141,12 +1514,32 @@ var NeonStorageBackend = class {
|
|
|
1141
1514
|
`SELECT * FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1142
1515
|
[userId, sessionId]
|
|
1143
1516
|
);
|
|
1144
|
-
|
|
1517
|
+
if (!rows[0]) return null;
|
|
1518
|
+
return this.mapRowToSessionData(rows[0]);
|
|
1145
1519
|
} catch (error) {
|
|
1146
1520
|
console.error("[NeonStorage] Failed to get session:", error);
|
|
1147
1521
|
return null;
|
|
1148
1522
|
}
|
|
1149
1523
|
}
|
|
1524
|
+
async getCredentials(userId, sessionId) {
|
|
1525
|
+
try {
|
|
1526
|
+
const credentialRows = await this.sql.query(
|
|
1527
|
+
`SELECT * FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1528
|
+
[userId, sessionId]
|
|
1529
|
+
);
|
|
1530
|
+
if (credentialRows[0]) {
|
|
1531
|
+
return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
|
|
1532
|
+
}
|
|
1533
|
+
const sessionRows = await this.sql.query(
|
|
1534
|
+
`SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1535
|
+
[userId, sessionId]
|
|
1536
|
+
);
|
|
1537
|
+
return sessionRows[0] ? { sessionId, userId } : null;
|
|
1538
|
+
} catch (error) {
|
|
1539
|
+
console.error("[NeonStorage] Failed to get credentials:", error);
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1150
1543
|
async list(userId) {
|
|
1151
1544
|
try {
|
|
1152
1545
|
const rows = await this.sql.query(
|
|
@@ -1159,6 +1552,16 @@ var NeonStorageBackend = class {
|
|
|
1159
1552
|
return [];
|
|
1160
1553
|
}
|
|
1161
1554
|
}
|
|
1555
|
+
async clearCredentials(userId, sessionId) {
|
|
1556
|
+
try {
|
|
1557
|
+
await this.sql.query(
|
|
1558
|
+
`DELETE FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1559
|
+
[userId, sessionId]
|
|
1560
|
+
);
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
console.error("[NeonStorage] Failed to clear credentials:", error);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1162
1565
|
async delete(userId, sessionId) {
|
|
1163
1566
|
try {
|
|
1164
1567
|
await this.sql.query(
|
|
@@ -1194,6 +1597,7 @@ var NeonStorageBackend = class {
|
|
|
1194
1597
|
}
|
|
1195
1598
|
async clearAll() {
|
|
1196
1599
|
try {
|
|
1600
|
+
await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
|
|
1197
1601
|
await this.sql.query(`DELETE FROM ${this.tableName}`);
|
|
1198
1602
|
} catch (error) {
|
|
1199
1603
|
console.error("[NeonStorage] Failed to clear sessions:", error);
|
|
@@ -1202,9 +1606,17 @@ var NeonStorageBackend = class {
|
|
|
1202
1606
|
async cleanupExpired() {
|
|
1203
1607
|
try {
|
|
1204
1608
|
await this.sql.query(
|
|
1205
|
-
`DELETE FROM ${this.tableName}
|
|
1609
|
+
`DELETE FROM ${this.tableName}
|
|
1610
|
+
WHERE expires_at IS NOT NULL
|
|
1611
|
+
AND expires_at < $1
|
|
1612
|
+
AND status <> 'active'`,
|
|
1206
1613
|
[(/* @__PURE__ */ new Date()).toISOString()]
|
|
1207
1614
|
);
|
|
1615
|
+
await this.sql.query(
|
|
1616
|
+
`DELETE FROM ${this.tableName}
|
|
1617
|
+
WHERE status = 'active' AND updated_at < $1`,
|
|
1618
|
+
[new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString()]
|
|
1619
|
+
);
|
|
1208
1620
|
} catch (error) {
|
|
1209
1621
|
console.error("[NeonStorage] Failed to cleanup expired sessions:", error);
|
|
1210
1622
|
}
|
|
@@ -1254,26 +1666,24 @@ function emitSessionMutation(event) {
|
|
|
1254
1666
|
function createSessionMutationEvent(prop, args) {
|
|
1255
1667
|
const timestamp = Date.now();
|
|
1256
1668
|
if (prop === "create") {
|
|
1257
|
-
const [session
|
|
1669
|
+
const [session] = args;
|
|
1258
1670
|
if (!session?.userId || !session?.sessionId) return null;
|
|
1259
1671
|
return {
|
|
1260
1672
|
type: "create",
|
|
1261
1673
|
userId: session.userId,
|
|
1262
1674
|
sessionId: session.sessionId,
|
|
1263
1675
|
session,
|
|
1264
|
-
ttl,
|
|
1265
1676
|
timestamp
|
|
1266
1677
|
};
|
|
1267
1678
|
}
|
|
1268
1679
|
if (prop === "update") {
|
|
1269
|
-
const [userId, sessionId, patch
|
|
1680
|
+
const [userId, sessionId, patch] = args;
|
|
1270
1681
|
if (!userId || !sessionId) return null;
|
|
1271
1682
|
return {
|
|
1272
1683
|
type: "update",
|
|
1273
1684
|
userId,
|
|
1274
1685
|
sessionId,
|
|
1275
1686
|
patch,
|
|
1276
|
-
ttl,
|
|
1277
1687
|
timestamp
|
|
1278
1688
|
};
|
|
1279
1689
|
}
|
|
@@ -1469,7 +1879,6 @@ var StorageOAuthClientProvider = class {
|
|
|
1469
1879
|
__publicField(this, "_authUrl");
|
|
1470
1880
|
__publicField(this, "_clientId");
|
|
1471
1881
|
__publicField(this, "onRedirectCallback");
|
|
1472
|
-
__publicField(this, "tokenExpiresAt");
|
|
1473
1882
|
this.userId = options.userId;
|
|
1474
1883
|
this.serverId = options.serverId;
|
|
1475
1884
|
this.sessionId = options.sessionId;
|
|
@@ -1503,30 +1912,30 @@ var StorageOAuthClientProvider = class {
|
|
|
1503
1912
|
this._clientId = clientId_;
|
|
1504
1913
|
}
|
|
1505
1914
|
/**
|
|
1506
|
-
* Loads OAuth
|
|
1915
|
+
* Loads OAuth credentials from the session store
|
|
1507
1916
|
* @private
|
|
1508
1917
|
*/
|
|
1509
|
-
async
|
|
1510
|
-
const data = await sessions.
|
|
1918
|
+
async getCredentials() {
|
|
1919
|
+
const data = await sessions.getCredentials(this.userId, this.sessionId);
|
|
1511
1920
|
if (!data) {
|
|
1512
|
-
return {};
|
|
1921
|
+
return { userId: this.userId, sessionId: this.sessionId };
|
|
1513
1922
|
}
|
|
1514
1923
|
return data;
|
|
1515
1924
|
}
|
|
1516
1925
|
/**
|
|
1517
|
-
* Saves OAuth
|
|
1518
|
-
* @param data - Partial OAuth
|
|
1926
|
+
* Saves OAuth credentials to the session store
|
|
1927
|
+
* @param data - Partial OAuth credentials to save
|
|
1519
1928
|
* @private
|
|
1520
1929
|
* @throws Error if session doesn't exist (session must be created by controller layer)
|
|
1521
1930
|
*/
|
|
1522
|
-
async
|
|
1523
|
-
await sessions.
|
|
1931
|
+
async patchCredentials(data) {
|
|
1932
|
+
await sessions.patchCredentials(this.userId, this.sessionId, data);
|
|
1524
1933
|
}
|
|
1525
1934
|
/**
|
|
1526
1935
|
* Retrieves stored OAuth client information
|
|
1527
1936
|
*/
|
|
1528
1937
|
async clientInformation() {
|
|
1529
|
-
const data = await this.
|
|
1938
|
+
const data = await this.getCredentials();
|
|
1530
1939
|
if (data.clientId && !this._clientId) {
|
|
1531
1940
|
this._clientId = data.clientId;
|
|
1532
1941
|
}
|
|
@@ -1545,7 +1954,7 @@ var StorageOAuthClientProvider = class {
|
|
|
1545
1954
|
* Stores OAuth client information
|
|
1546
1955
|
*/
|
|
1547
1956
|
async saveClientInformation(clientInformation) {
|
|
1548
|
-
await this.
|
|
1957
|
+
await this.patchCredentials({
|
|
1549
1958
|
clientInformation,
|
|
1550
1959
|
clientId: clientInformation.client_id
|
|
1551
1960
|
});
|
|
@@ -1555,29 +1964,58 @@ var StorageOAuthClientProvider = class {
|
|
|
1555
1964
|
* Stores OAuth tokens
|
|
1556
1965
|
*/
|
|
1557
1966
|
async saveTokens(tokens) {
|
|
1558
|
-
|
|
1559
|
-
if (tokens.expires_in) {
|
|
1560
|
-
this.tokenExpiresAt = Date.now() + tokens.expires_in * 1e3 - TOKEN_EXPIRY_BUFFER_MS;
|
|
1561
|
-
}
|
|
1562
|
-
await this.saveSessionData(data);
|
|
1967
|
+
await this.patchCredentials({ tokens });
|
|
1563
1968
|
}
|
|
1564
1969
|
get authUrl() {
|
|
1565
1970
|
return this._authUrl;
|
|
1566
1971
|
}
|
|
1567
1972
|
async state() {
|
|
1568
|
-
|
|
1973
|
+
const nonce = nanoid.nanoid(32);
|
|
1974
|
+
await this.patchCredentials({
|
|
1975
|
+
oauthState: {
|
|
1976
|
+
nonce,
|
|
1977
|
+
sessionId: this.sessionId,
|
|
1978
|
+
serverId: this.serverId,
|
|
1979
|
+
createdAt: Date.now()
|
|
1980
|
+
},
|
|
1981
|
+
codeVerifier: null
|
|
1982
|
+
});
|
|
1983
|
+
return formatOAuthState(nonce, this.sessionId);
|
|
1569
1984
|
}
|
|
1570
|
-
async checkState(
|
|
1571
|
-
const
|
|
1985
|
+
async checkState(state) {
|
|
1986
|
+
const parsed = parseOAuthState(state);
|
|
1987
|
+
if (!parsed) {
|
|
1988
|
+
return { valid: false, error: "Invalid OAuth state" };
|
|
1989
|
+
}
|
|
1990
|
+
if (parsed.sessionId !== this.sessionId) {
|
|
1991
|
+
return { valid: false, error: "OAuth state mismatch" };
|
|
1992
|
+
}
|
|
1993
|
+
const data = await sessions.getCredentials(this.userId, parsed.sessionId);
|
|
1572
1994
|
if (!data) {
|
|
1573
1995
|
return { valid: false, error: "Session not found" };
|
|
1574
1996
|
}
|
|
1575
|
-
|
|
1997
|
+
const oauthState = data.oauthState;
|
|
1998
|
+
if (!oauthState) {
|
|
1999
|
+
return { valid: false, error: "OAuth state not found" };
|
|
2000
|
+
}
|
|
2001
|
+
if (oauthState.nonce !== parsed.nonce || oauthState.sessionId !== parsed.sessionId || oauthState.serverId !== this.serverId) {
|
|
2002
|
+
return { valid: false, error: "OAuth state mismatch" };
|
|
2003
|
+
}
|
|
2004
|
+
if (Date.now() - oauthState.createdAt > STATE_EXPIRATION_MS) {
|
|
2005
|
+
return { valid: false, error: "OAuth state expired" };
|
|
2006
|
+
}
|
|
2007
|
+
return { valid: true, serverId: oauthState.serverId };
|
|
1576
2008
|
}
|
|
1577
|
-
async consumeState(
|
|
2009
|
+
async consumeState(state) {
|
|
2010
|
+
const result = await this.checkState(state);
|
|
2011
|
+
if (!result.valid) {
|
|
2012
|
+
throw new Error(result.error || "Invalid OAuth state");
|
|
2013
|
+
}
|
|
2014
|
+
await this.patchCredentials({ oauthState: null });
|
|
1578
2015
|
}
|
|
1579
2016
|
async redirectToAuthorization(authUrl) {
|
|
1580
2017
|
this._authUrl = authUrl.toString();
|
|
2018
|
+
await sessions.update(this.userId, this.sessionId, { authUrl: this._authUrl });
|
|
1581
2019
|
if (this.onRedirectCallback) {
|
|
1582
2020
|
this.onRedirectCallback(authUrl.toString());
|
|
1583
2021
|
}
|
|
@@ -1588,21 +2026,25 @@ var StorageOAuthClientProvider = class {
|
|
|
1588
2026
|
} else {
|
|
1589
2027
|
const updates = {};
|
|
1590
2028
|
if (scope === "client") {
|
|
1591
|
-
updates.clientInformation =
|
|
1592
|
-
updates.clientId =
|
|
2029
|
+
updates.clientInformation = null;
|
|
2030
|
+
updates.clientId = null;
|
|
1593
2031
|
} else if (scope === "tokens") {
|
|
1594
|
-
updates.tokens =
|
|
2032
|
+
updates.tokens = null;
|
|
1595
2033
|
} else if (scope === "verifier") {
|
|
1596
|
-
updates.codeVerifier =
|
|
2034
|
+
updates.codeVerifier = null;
|
|
1597
2035
|
}
|
|
1598
|
-
await this.
|
|
2036
|
+
await this.patchCredentials(updates);
|
|
1599
2037
|
}
|
|
1600
2038
|
}
|
|
1601
2039
|
async saveCodeVerifier(verifier) {
|
|
1602
|
-
await this.
|
|
2040
|
+
const data = await this.getCredentials();
|
|
2041
|
+
if (data.codeVerifier) {
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
await this.patchCredentials({ codeVerifier: verifier });
|
|
1603
2045
|
}
|
|
1604
2046
|
async codeVerifier() {
|
|
1605
|
-
const data = await this.
|
|
2047
|
+
const data = await this.getCredentials();
|
|
1606
2048
|
if (data.clientId && !this._clientId) {
|
|
1607
2049
|
this._clientId = data.clientId;
|
|
1608
2050
|
}
|
|
@@ -1612,23 +2054,14 @@ var StorageOAuthClientProvider = class {
|
|
|
1612
2054
|
return data.codeVerifier;
|
|
1613
2055
|
}
|
|
1614
2056
|
async deleteCodeVerifier() {
|
|
1615
|
-
await this.
|
|
2057
|
+
await this.patchCredentials({ codeVerifier: null });
|
|
1616
2058
|
}
|
|
1617
2059
|
async tokens() {
|
|
1618
|
-
const data = await this.
|
|
2060
|
+
const data = await this.getCredentials();
|
|
1619
2061
|
if (data.clientId && !this._clientId) {
|
|
1620
2062
|
this._clientId = data.clientId;
|
|
1621
2063
|
}
|
|
1622
|
-
return data.tokens;
|
|
1623
|
-
}
|
|
1624
|
-
isTokenExpired() {
|
|
1625
|
-
if (!this.tokenExpiresAt) {
|
|
1626
|
-
return false;
|
|
1627
|
-
}
|
|
1628
|
-
return Date.now() >= this.tokenExpiresAt;
|
|
1629
|
-
}
|
|
1630
|
-
setTokenExpiresAt(expiresAt) {
|
|
1631
|
-
this.tokenExpiresAt = expiresAt;
|
|
2064
|
+
return data.tokens ?? void 0;
|
|
1632
2065
|
}
|
|
1633
2066
|
};
|
|
1634
2067
|
|
|
@@ -1709,14 +2142,7 @@ var RpcErrorCodes = {
|
|
|
1709
2142
|
EXECUTION_ERROR: "EXECUTION_ERROR"};
|
|
1710
2143
|
|
|
1711
2144
|
// src/server/mcp/oauth-client.ts
|
|
1712
|
-
|
|
1713
|
-
if (!(error instanceof Error)) {
|
|
1714
|
-
return false;
|
|
1715
|
-
}
|
|
1716
|
-
const text = `${error.name} ${error.message}`.toLowerCase();
|
|
1717
|
-
return text.includes("invalidgrant") || text.includes("invalid_grant") || text.includes("invalid refresh token") || /refresh\s+token\s+(?:is\s+)?(?:invalid|expired|revoked)/i.test(text);
|
|
1718
|
-
}
|
|
1719
|
-
var MCPClient = class _MCPClient {
|
|
2145
|
+
var MCPClient = class {
|
|
1720
2146
|
/**
|
|
1721
2147
|
* Creates a new MCP client instance
|
|
1722
2148
|
* Can be initialized with minimal options (userId + sessionId) for session restoration
|
|
@@ -1891,17 +2317,18 @@ var MCPClient = class _MCPClient {
|
|
|
1891
2317
|
}
|
|
1892
2318
|
this.emitStateChange("INITIALIZING");
|
|
1893
2319
|
this.emitProgress("Loading session configuration...");
|
|
2320
|
+
let existingSession = null;
|
|
1894
2321
|
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
1895
|
-
|
|
1896
|
-
if (!
|
|
2322
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2323
|
+
if (!existingSession) {
|
|
1897
2324
|
throw new Error(`Session not found: ${this.sessionId}`);
|
|
1898
2325
|
}
|
|
1899
|
-
this.serverUrl = this.serverUrl ||
|
|
1900
|
-
this.callbackUrl = this.callbackUrl ||
|
|
1901
|
-
this.serverName = this.serverName ||
|
|
1902
|
-
this.serverId = this.serverId ||
|
|
1903
|
-
this.headers = this.headers ||
|
|
1904
|
-
this.createdAt =
|
|
2326
|
+
this.serverUrl = this.serverUrl || existingSession.serverUrl;
|
|
2327
|
+
this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
|
|
2328
|
+
this.serverName = this.serverName || existingSession.serverName;
|
|
2329
|
+
this.serverId = this.serverId || existingSession.serverId || "unknown";
|
|
2330
|
+
this.headers = this.headers || existingSession.headers;
|
|
2331
|
+
this.createdAt = existingSession.createdAt;
|
|
1905
2332
|
}
|
|
1906
2333
|
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
1907
2334
|
throw new Error("Missing required connection metadata");
|
|
@@ -1945,10 +2372,13 @@ var MCPClient = class _MCPClient {
|
|
|
1945
2372
|
}
|
|
1946
2373
|
);
|
|
1947
2374
|
}
|
|
1948
|
-
|
|
2375
|
+
if (existingSession === null) {
|
|
2376
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2377
|
+
}
|
|
1949
2378
|
if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
|
|
1950
2379
|
this.createdAt = Date.now();
|
|
1951
|
-
|
|
2380
|
+
const updatedAt = this.createdAt;
|
|
2381
|
+
console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
|
|
1952
2382
|
await sessions.create({
|
|
1953
2383
|
sessionId: this.sessionId,
|
|
1954
2384
|
userId: this.userId,
|
|
@@ -1959,18 +2389,18 @@ var MCPClient = class _MCPClient {
|
|
|
1959
2389
|
transportType: this.transportType || "streamable-http",
|
|
1960
2390
|
headers: this.headers,
|
|
1961
2391
|
createdAt: this.createdAt,
|
|
1962
|
-
|
|
1963
|
-
|
|
2392
|
+
updatedAt,
|
|
2393
|
+
status: "pending"
|
|
2394
|
+
});
|
|
1964
2395
|
}
|
|
1965
2396
|
}
|
|
1966
2397
|
/**
|
|
1967
2398
|
* Saves current session state to the session store
|
|
1968
2399
|
* Creates new session if it doesn't exist, updates if it does
|
|
1969
|
-
* @param
|
|
1970
|
-
* @param active - Session status marker used to avoid unnecessary TTL rewrites
|
|
2400
|
+
* @param status - Session lifecycle status used by storage cleanup
|
|
1971
2401
|
* @private
|
|
1972
2402
|
*/
|
|
1973
|
-
async saveSession(
|
|
2403
|
+
async saveSession(status = "active", existingSession) {
|
|
1974
2404
|
if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
|
|
1975
2405
|
return;
|
|
1976
2406
|
}
|
|
@@ -1984,13 +2414,29 @@ var MCPClient = class _MCPClient {
|
|
|
1984
2414
|
transportType: this.transportType || "streamable-http",
|
|
1985
2415
|
headers: this.headers,
|
|
1986
2416
|
createdAt: this.createdAt || Date.now(),
|
|
1987
|
-
|
|
2417
|
+
updatedAt: Date.now(),
|
|
2418
|
+
status
|
|
1988
2419
|
};
|
|
1989
|
-
|
|
2420
|
+
if (status === "active") {
|
|
2421
|
+
sessionData.authUrl = null;
|
|
2422
|
+
}
|
|
2423
|
+
if (existingSession === void 0) {
|
|
2424
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2425
|
+
}
|
|
1990
2426
|
if (existingSession) {
|
|
1991
|
-
await sessions.update(this.userId, this.sessionId, sessionData
|
|
2427
|
+
await sessions.update(this.userId, this.sessionId, sessionData);
|
|
1992
2428
|
} else {
|
|
1993
|
-
await sessions.create(sessionData
|
|
2429
|
+
await sessions.create(sessionData);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Removes transient setup/auth sessions without masking the original error.
|
|
2434
|
+
* @private
|
|
2435
|
+
*/
|
|
2436
|
+
async deleteTransientSession() {
|
|
2437
|
+
try {
|
|
2438
|
+
await sessions.delete(this.userId, this.sessionId);
|
|
2439
|
+
} catch {
|
|
1994
2440
|
}
|
|
1995
2441
|
}
|
|
1996
2442
|
/**
|
|
@@ -2050,15 +2496,13 @@ var MCPClient = class _MCPClient {
|
|
|
2050
2496
|
throw new Error(error);
|
|
2051
2497
|
}
|
|
2052
2498
|
try {
|
|
2053
|
-
this.emitProgress("Validating OAuth tokens...");
|
|
2054
|
-
await this.getValidTokens();
|
|
2055
2499
|
this.emitStateChange("CONNECTING");
|
|
2056
2500
|
const { transportType } = await this.tryConnect();
|
|
2057
2501
|
this.transportType = transportType;
|
|
2058
2502
|
this.emitStateChange("CONNECTED");
|
|
2059
2503
|
this.emitProgress("Connected successfully");
|
|
2060
|
-
console.log(`[MCPClient] Saving session ${this.sessionId}
|
|
2061
|
-
await this.saveSession(
|
|
2504
|
+
console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
|
|
2505
|
+
await this.saveSession("active");
|
|
2062
2506
|
} catch (error) {
|
|
2063
2507
|
if (error instanceof auth_js.UnauthorizedError || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
|
|
2064
2508
|
let authUrl = "";
|
|
@@ -2070,15 +2514,12 @@ var MCPClient = class _MCPClient {
|
|
|
2070
2514
|
const message = detail.toLowerCase() === "unauthorized" ? "OAuth authorization URL not available" : `OAuth authorization URL not available: ${detail}`;
|
|
2071
2515
|
this.emitError(message, "auth");
|
|
2072
2516
|
this.emitStateChange("FAILED");
|
|
2073
|
-
|
|
2074
|
-
await sessions.delete(this.userId, this.sessionId);
|
|
2075
|
-
} catch {
|
|
2076
|
-
}
|
|
2517
|
+
await this.deleteTransientSession();
|
|
2077
2518
|
throw new Error(message);
|
|
2078
2519
|
}
|
|
2079
2520
|
this.emitStateChange("AUTHENTICATING");
|
|
2080
|
-
console.log(`[MCPClient] Saving session ${this.sessionId}
|
|
2081
|
-
await this.saveSession(
|
|
2521
|
+
console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
|
|
2522
|
+
await this.saveSession("pending");
|
|
2082
2523
|
if (this.serverId) {
|
|
2083
2524
|
this._onConnectionEvent.fire({
|
|
2084
2525
|
type: "auth_required",
|
|
@@ -2098,7 +2539,7 @@ var MCPClient = class _MCPClient {
|
|
|
2098
2539
|
this.emitStateChange("FAILED");
|
|
2099
2540
|
try {
|
|
2100
2541
|
const existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2101
|
-
if (!existingSession || existingSession.
|
|
2542
|
+
if (!existingSession || existingSession.status !== "active") {
|
|
2102
2543
|
await sessions.delete(this.userId, this.sessionId);
|
|
2103
2544
|
}
|
|
2104
2545
|
} catch {
|
|
@@ -2113,7 +2554,7 @@ var MCPClient = class _MCPClient {
|
|
|
2113
2554
|
* @param authCode - Authorization code received from OAuth callback
|
|
2114
2555
|
*/
|
|
2115
2556
|
// TODO: needs to be optimized
|
|
2116
|
-
async finishAuth(authCode) {
|
|
2557
|
+
async finishAuth(authCode, state) {
|
|
2117
2558
|
this.emitStateChange("AUTHENTICATING");
|
|
2118
2559
|
this.emitProgress("Exchanging authorization code for tokens...");
|
|
2119
2560
|
await this.initialize();
|
|
@@ -2123,6 +2564,16 @@ var MCPClient = class _MCPClient {
|
|
|
2123
2564
|
this.emitStateChange("FAILED");
|
|
2124
2565
|
throw new Error(error);
|
|
2125
2566
|
}
|
|
2567
|
+
if (state) {
|
|
2568
|
+
const stateCheck = await this.oauthProvider.checkState(state);
|
|
2569
|
+
if (!stateCheck.valid) {
|
|
2570
|
+
const error = stateCheck.error || "Invalid OAuth state";
|
|
2571
|
+
this.emitError(error, "auth");
|
|
2572
|
+
this.emitStateChange("FAILED");
|
|
2573
|
+
throw new Error(error);
|
|
2574
|
+
}
|
|
2575
|
+
await this.oauthProvider.consumeState(state);
|
|
2576
|
+
}
|
|
2126
2577
|
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2127
2578
|
let lastError;
|
|
2128
2579
|
let tokensExchanged = false;
|
|
@@ -2162,8 +2613,8 @@ var MCPClient = class _MCPClient {
|
|
|
2162
2613
|
await this.client.connect(this.transport);
|
|
2163
2614
|
this.transportType = currentType;
|
|
2164
2615
|
this.emitStateChange("CONNECTED");
|
|
2165
|
-
console.log(`[MCPClient]
|
|
2166
|
-
await this.saveSession(
|
|
2616
|
+
console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
|
|
2617
|
+
await this.saveSession("active");
|
|
2167
2618
|
return;
|
|
2168
2619
|
} catch (error) {
|
|
2169
2620
|
lastError = error;
|
|
@@ -2176,12 +2627,14 @@ var MCPClient = class _MCPClient {
|
|
|
2176
2627
|
const msg = error instanceof Error ? error.message : "Authentication failed";
|
|
2177
2628
|
this.emitError(msg, "auth");
|
|
2178
2629
|
this.emitStateChange("FAILED");
|
|
2630
|
+
await this.deleteTransientSession();
|
|
2179
2631
|
throw error;
|
|
2180
2632
|
}
|
|
2181
2633
|
if (isLastAttempt) {
|
|
2182
2634
|
const msg = error instanceof Error ? error.message : "Authentication failed";
|
|
2183
2635
|
this.emitError(msg, "auth");
|
|
2184
2636
|
this.emitStateChange("FAILED");
|
|
2637
|
+
await this.deleteTransientSession();
|
|
2185
2638
|
throw error;
|
|
2186
2639
|
}
|
|
2187
2640
|
this.emitProgress(`Auth attempt with ${currentType} failed: ${errorMessage}. Retrying...`);
|
|
@@ -2191,6 +2644,7 @@ var MCPClient = class _MCPClient {
|
|
|
2191
2644
|
const errorMessage = lastError instanceof Error ? lastError.message : "Authentication failed";
|
|
2192
2645
|
this.emitError(errorMessage, "auth");
|
|
2193
2646
|
this.emitStateChange("FAILED");
|
|
2647
|
+
await this.deleteTransientSession();
|
|
2194
2648
|
throw lastError;
|
|
2195
2649
|
}
|
|
2196
2650
|
}
|
|
@@ -2376,67 +2830,6 @@ var MCPClient = class _MCPClient {
|
|
|
2376
2830
|
};
|
|
2377
2831
|
return await this.client.request(request, types_js.ReadResourceResultSchema);
|
|
2378
2832
|
}
|
|
2379
|
-
/**
|
|
2380
|
-
* Refreshes the OAuth access token using the refresh token
|
|
2381
|
-
* Discovers OAuth metadata from server and exchanges refresh token for new access token
|
|
2382
|
-
* @returns True if refresh was successful, false otherwise
|
|
2383
|
-
*/
|
|
2384
|
-
async refreshToken() {
|
|
2385
|
-
await this.initialize();
|
|
2386
|
-
if (!this.oauthProvider) {
|
|
2387
|
-
return false;
|
|
2388
|
-
}
|
|
2389
|
-
const tokens = await this.oauthProvider.tokens();
|
|
2390
|
-
if (!tokens || !tokens.refresh_token) {
|
|
2391
|
-
return false;
|
|
2392
|
-
}
|
|
2393
|
-
const clientInformation = await this.oauthProvider.clientInformation();
|
|
2394
|
-
if (!clientInformation) {
|
|
2395
|
-
return false;
|
|
2396
|
-
}
|
|
2397
|
-
try {
|
|
2398
|
-
const resourceMetadata = await auth_js.discoverOAuthProtectedResourceMetadata(this.serverUrl);
|
|
2399
|
-
const authServerUrl = resourceMetadata?.authorization_servers?.[0] || this.serverUrl;
|
|
2400
|
-
const authMetadata = await auth_js.discoverAuthorizationServerMetadata(authServerUrl);
|
|
2401
|
-
const newTokens = await auth_js.refreshAuthorization(authServerUrl, {
|
|
2402
|
-
metadata: authMetadata,
|
|
2403
|
-
clientInformation,
|
|
2404
|
-
refreshToken: tokens.refresh_token
|
|
2405
|
-
});
|
|
2406
|
-
await this.oauthProvider.saveTokens(newTokens);
|
|
2407
|
-
return true;
|
|
2408
|
-
} catch (error) {
|
|
2409
|
-
console.error("[OAuth] Token refresh failed:", error);
|
|
2410
|
-
if (isInvalidRefreshTokenError(error)) {
|
|
2411
|
-
try {
|
|
2412
|
-
await this.oauthProvider.invalidateCredentials?.("tokens");
|
|
2413
|
-
this.emitProgress("OAuth refresh token is invalid; requesting reauthorization...");
|
|
2414
|
-
} catch (invalidateError) {
|
|
2415
|
-
console.warn("[OAuth] Failed to invalidate stale refresh token credentials:", invalidateError);
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
return false;
|
|
2419
|
-
}
|
|
2420
|
-
}
|
|
2421
|
-
/**
|
|
2422
|
-
* Ensures OAuth tokens are valid, refreshing them if expired
|
|
2423
|
-
* Called automatically by connect() - rarely needs to be called manually
|
|
2424
|
-
* @returns True if valid tokens are available, false otherwise
|
|
2425
|
-
*/
|
|
2426
|
-
async getValidTokens() {
|
|
2427
|
-
await this.initialize();
|
|
2428
|
-
if (!this.oauthProvider) {
|
|
2429
|
-
return false;
|
|
2430
|
-
}
|
|
2431
|
-
const tokens = await this.oauthProvider.tokens();
|
|
2432
|
-
if (!tokens) {
|
|
2433
|
-
return false;
|
|
2434
|
-
}
|
|
2435
|
-
if (this.oauthProvider.isTokenExpired()) {
|
|
2436
|
-
return await this.refreshToken();
|
|
2437
|
-
}
|
|
2438
|
-
return true;
|
|
2439
|
-
}
|
|
2440
2833
|
/**
|
|
2441
2834
|
* Reconnects to MCP server using existing OAuth provider from Redis
|
|
2442
2835
|
* Used for session restoration in serverless environments
|
|
@@ -2566,68 +2959,6 @@ var MCPClient = class _MCPClient {
|
|
|
2566
2959
|
getSessionId() {
|
|
2567
2960
|
return this.sessionId;
|
|
2568
2961
|
}
|
|
2569
|
-
/**
|
|
2570
|
-
* Gets MCP server configuration for all active user sessions
|
|
2571
|
-
* Loads sessions from Redis, validates OAuth tokens, refreshes if expired
|
|
2572
|
-
* Returns ready-to-use configuration with valid auth headers
|
|
2573
|
-
* @param userId - User ID to fetch sessions for
|
|
2574
|
-
* @returns Object keyed by sanitized server labels containing transport, url, headers, etc.
|
|
2575
|
-
* @static
|
|
2576
|
-
*/
|
|
2577
|
-
static async getMcpServerConfig(userId) {
|
|
2578
|
-
const mcpConfig = {};
|
|
2579
|
-
const sessionList = await sessions.list(userId);
|
|
2580
|
-
await Promise.all(
|
|
2581
|
-
sessionList.map(async (sessionData) => {
|
|
2582
|
-
const { sessionId } = sessionData;
|
|
2583
|
-
try {
|
|
2584
|
-
if (!sessionData.serverId || !sessionData.transportType || !sessionData.serverUrl || !sessionData.callbackUrl) {
|
|
2585
|
-
await sessions.delete(userId, sessionId);
|
|
2586
|
-
return;
|
|
2587
|
-
}
|
|
2588
|
-
let headers;
|
|
2589
|
-
try {
|
|
2590
|
-
const client = new _MCPClient({
|
|
2591
|
-
userId,
|
|
2592
|
-
sessionId,
|
|
2593
|
-
serverId: sessionData.serverId,
|
|
2594
|
-
serverUrl: sessionData.serverUrl,
|
|
2595
|
-
callbackUrl: sessionData.callbackUrl,
|
|
2596
|
-
serverName: sessionData.serverName,
|
|
2597
|
-
transportType: sessionData.transportType,
|
|
2598
|
-
headers: sessionData.headers
|
|
2599
|
-
});
|
|
2600
|
-
await client.initialize();
|
|
2601
|
-
const hasValidTokens = await client.getValidTokens();
|
|
2602
|
-
if (hasValidTokens && client.oauthProvider) {
|
|
2603
|
-
const tokens = await client.oauthProvider.tokens();
|
|
2604
|
-
if (tokens?.access_token) {
|
|
2605
|
-
headers = { Authorization: `Bearer ${tokens.access_token}` };
|
|
2606
|
-
}
|
|
2607
|
-
}
|
|
2608
|
-
} catch (error) {
|
|
2609
|
-
console.warn(`[MCP] Failed to get OAuth tokens for ${sessionId}:`, error);
|
|
2610
|
-
}
|
|
2611
|
-
const label = sanitizeServerLabel(
|
|
2612
|
-
sessionData.serverName || sessionData.serverId || "server"
|
|
2613
|
-
);
|
|
2614
|
-
mcpConfig[label] = {
|
|
2615
|
-
transport: sessionData.transportType,
|
|
2616
|
-
url: sessionData.serverUrl,
|
|
2617
|
-
...sessionData.serverName && {
|
|
2618
|
-
serverName: sessionData.serverName,
|
|
2619
|
-
serverLabel: label
|
|
2620
|
-
},
|
|
2621
|
-
...headers && { headers }
|
|
2622
|
-
};
|
|
2623
|
-
} catch (error) {
|
|
2624
|
-
await sessions.delete(userId, sessionId);
|
|
2625
|
-
console.warn(`[MCP] Failed to process session ${sessionId}:`, error);
|
|
2626
|
-
}
|
|
2627
|
-
})
|
|
2628
|
-
);
|
|
2629
|
-
return mcpConfig;
|
|
2630
|
-
}
|
|
2631
2962
|
};
|
|
2632
2963
|
|
|
2633
2964
|
// src/server/mcp/multi-session-client.ts
|
|
@@ -2663,18 +2994,13 @@ var MultiSessionClient = class {
|
|
|
2663
2994
|
*
|
|
2664
2995
|
* A session is considered connectable when:
|
|
2665
2996
|
* - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
|
|
2666
|
-
* - Its
|
|
2667
|
-
* either mid-OAuth flow, auth-pending, or previously failed. We skip those here
|
|
2997
|
+
* - Its status is `active`. Pending sessions are skipped here
|
|
2668
2998
|
* and let the OAuth flow complete separately before we try to reconnect them.
|
|
2669
|
-
*
|
|
2670
|
-
* Note: Sessions where `active` is `undefined` (legacy records) are included
|
|
2671
|
-
* for backwards compatibility.
|
|
2672
2999
|
*/
|
|
2673
3000
|
async getActiveSessions() {
|
|
2674
3001
|
const sessionList = await sessions.list(this.userId);
|
|
2675
3002
|
const valid = sessionList.filter(
|
|
2676
|
-
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.
|
|
2677
|
-
// exclude OAuth-pending / failed sessions
|
|
3003
|
+
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
|
|
2678
3004
|
);
|
|
2679
3005
|
return valid;
|
|
2680
3006
|
}
|
|
@@ -2949,7 +3275,8 @@ var SSEConnectionManager = class {
|
|
|
2949
3275
|
serverUrl: s.serverUrl,
|
|
2950
3276
|
transport: s.transportType,
|
|
2951
3277
|
createdAt: s.createdAt,
|
|
2952
|
-
|
|
3278
|
+
updatedAt: s.updatedAt ?? s.createdAt,
|
|
3279
|
+
status: s.status ?? "pending"
|
|
2953
3280
|
}))
|
|
2954
3281
|
};
|
|
2955
3282
|
}
|
|
@@ -2965,7 +3292,7 @@ var SSEConnectionManager = class {
|
|
|
2965
3292
|
(s) => s.serverId === serverId || s.serverUrl === serverUrl
|
|
2966
3293
|
);
|
|
2967
3294
|
if (duplicate) {
|
|
2968
|
-
if (duplicate.
|
|
3295
|
+
if (duplicate.status === "pending") {
|
|
2969
3296
|
await this.getSession({ sessionId: duplicate.sessionId });
|
|
2970
3297
|
return {
|
|
2971
3298
|
sessionId: duplicate.sessionId,
|
|
@@ -3152,7 +3479,10 @@ var SSEConnectionManager = class {
|
|
|
3152
3479
|
* Complete OAuth authorization flow
|
|
3153
3480
|
*/
|
|
3154
3481
|
async finishAuth(params) {
|
|
3155
|
-
const {
|
|
3482
|
+
const { code } = params;
|
|
3483
|
+
const oauthState = params.state;
|
|
3484
|
+
const parsedState = parseOAuthState(oauthState);
|
|
3485
|
+
const sessionId = parsedState?.sessionId || oauthState;
|
|
3156
3486
|
const session = await sessions.get(this.userId, sessionId);
|
|
3157
3487
|
if (!session) {
|
|
3158
3488
|
throw new Error("Session not found");
|
|
@@ -3177,7 +3507,7 @@ var SSEConnectionManager = class {
|
|
|
3177
3507
|
headers: session.headers
|
|
3178
3508
|
});
|
|
3179
3509
|
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
3180
|
-
await client.finishAuth(code);
|
|
3510
|
+
await client.finishAuth(code, oauthState);
|
|
3181
3511
|
this.clients.set(sessionId, client);
|
|
3182
3512
|
const tools = await client.listTools();
|
|
3183
3513
|
return { success: true, toolCount: tools.tools.length };
|