@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.mjs
CHANGED
|
@@ -112,21 +112,7 @@ var init_redis = __esm({
|
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
114
|
});
|
|
115
|
-
|
|
116
|
-
// src/shared/constants.ts
|
|
117
|
-
var SESSION_TTL_SECONDS = 43200;
|
|
118
|
-
var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
|
|
119
|
-
var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
120
|
-
var REDIS_KEY_PREFIX = "mcp:session:";
|
|
121
|
-
var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
122
|
-
var DEFAULT_CLIENT_NAME = "MCP Assistant";
|
|
123
|
-
var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
|
|
124
|
-
var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
|
|
125
|
-
var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
|
|
126
|
-
var SOFTWARE_ID = "@mcp-ts";
|
|
127
|
-
var SOFTWARE_VERSION = "1.3.4";
|
|
128
|
-
var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
|
|
129
|
-
var MCP_CLIENT_VERSION = "2.0";
|
|
115
|
+
var OAUTH_STATE_SEPARATOR = ".";
|
|
130
116
|
var firstChar = customAlphabet(
|
|
131
117
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
|
132
118
|
1
|
|
@@ -145,13 +131,98 @@ function sanitizeServerLabel(name) {
|
|
|
145
131
|
function generateSessionId() {
|
|
146
132
|
return firstChar() + rest();
|
|
147
133
|
}
|
|
134
|
+
function formatOAuthState(nonce, sessionId) {
|
|
135
|
+
return `${nonce}${OAUTH_STATE_SEPARATOR}${sessionId}`;
|
|
136
|
+
}
|
|
137
|
+
function parseOAuthState(state) {
|
|
138
|
+
const separatorIndex = state.indexOf(OAUTH_STATE_SEPARATOR);
|
|
139
|
+
if (separatorIndex <= 0 || separatorIndex === state.length - 1) {
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
const nonce = state.slice(0, separatorIndex);
|
|
143
|
+
const sessionId = state.slice(separatorIndex + 1);
|
|
144
|
+
if (!nonce || !sessionId || sessionId.includes(OAUTH_STATE_SEPARATOR)) {
|
|
145
|
+
return void 0;
|
|
146
|
+
}
|
|
147
|
+
return { nonce, sessionId };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/shared/constants.ts
|
|
151
|
+
var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
|
|
152
|
+
var PENDING_SESSION_EXPIRATION_SECONDS = Math.floor(STATE_EXPIRATION_MS / 1e3);
|
|
153
|
+
var DORMANT_SESSION_EXPIRATION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
154
|
+
var DORMANT_SESSION_EXPIRATION_SECONDS = Math.floor(DORMANT_SESSION_EXPIRATION_MS / 1e3);
|
|
155
|
+
var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
156
|
+
var REDIS_KEY_PREFIX = "mcp:session:";
|
|
157
|
+
var DEFAULT_CLIENT_NAME = "MCP Assistant";
|
|
158
|
+
var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
|
|
159
|
+
var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
|
|
160
|
+
var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
|
|
161
|
+
var SOFTWARE_ID = "@mcp-ts";
|
|
162
|
+
var SOFTWARE_VERSION = "2.3.4";
|
|
163
|
+
var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
|
|
164
|
+
var MCP_CLIENT_VERSION = "2.0";
|
|
165
|
+
|
|
166
|
+
// src/server/storage/session-lifecycle.ts
|
|
167
|
+
function resolveSessionExpiresAt(status = "pending", referenceTime = Date.now()) {
|
|
168
|
+
return status === "active" ? null : referenceTime + STATE_EXPIRATION_MS;
|
|
169
|
+
}
|
|
170
|
+
function resolveSessionRedisTtlSeconds(session) {
|
|
171
|
+
return session.status === "active" ? DORMANT_SESSION_EXPIRATION_SECONDS : Math.floor(STATE_EXPIRATION_MS / 1e3);
|
|
172
|
+
}
|
|
173
|
+
function normalizeNewSession(session, now = Date.now()) {
|
|
174
|
+
const createdAt = session.createdAt || now;
|
|
175
|
+
const updatedAt = session.updatedAt ?? createdAt;
|
|
176
|
+
const status = session.status ?? "pending";
|
|
177
|
+
return {
|
|
178
|
+
...session,
|
|
179
|
+
status,
|
|
180
|
+
createdAt,
|
|
181
|
+
updatedAt,
|
|
182
|
+
expiresAt: resolveSessionExpiresAt(status, status === "active" ? updatedAt : createdAt)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function mergeSessionUpdate(current, data, now = Date.now()) {
|
|
186
|
+
const updatedAt = data.updatedAt ?? now;
|
|
187
|
+
const updated = {
|
|
188
|
+
...current,
|
|
189
|
+
...data,
|
|
190
|
+
updatedAt
|
|
191
|
+
};
|
|
192
|
+
const status = updated.status ?? "pending";
|
|
193
|
+
return {
|
|
194
|
+
...updated,
|
|
195
|
+
status,
|
|
196
|
+
expiresAt: resolveSessionExpiresAt(status, updatedAt)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function normalizeStoredSession(session) {
|
|
200
|
+
const createdAt = session.createdAt || Date.now();
|
|
201
|
+
const updatedAt = session.updatedAt ?? createdAt;
|
|
202
|
+
const status = session.status ?? "pending";
|
|
203
|
+
const expiresAt = status === "active" ? null : session.expiresAt ?? resolveSessionExpiresAt(status, createdAt);
|
|
204
|
+
return {
|
|
205
|
+
...session,
|
|
206
|
+
status,
|
|
207
|
+
createdAt,
|
|
208
|
+
updatedAt,
|
|
209
|
+
expiresAt
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function isSessionExpired(session, now = Date.now()) {
|
|
213
|
+
const hydrated = normalizeStoredSession(session);
|
|
214
|
+
if (hydrated.status === "active") {
|
|
215
|
+
return hydrated.updatedAt !== void 0 && hydrated.updatedAt < now - DORMANT_SESSION_EXPIRATION_MS;
|
|
216
|
+
}
|
|
217
|
+
return typeof hydrated.expiresAt === "number" && hydrated.expiresAt < now;
|
|
218
|
+
}
|
|
148
219
|
|
|
149
220
|
// src/server/storage/redis-backend.ts
|
|
150
221
|
var RedisStorageBackend = class {
|
|
151
222
|
constructor(redis2) {
|
|
152
223
|
this.redis = redis2;
|
|
153
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
154
224
|
__publicField(this, "KEY_PREFIX", "mcp:session:");
|
|
225
|
+
__publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
|
|
155
226
|
__publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
|
|
156
227
|
__publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
|
|
157
228
|
}
|
|
@@ -170,6 +241,9 @@ var RedisStorageBackend = class {
|
|
|
170
241
|
getSessionKey(userId, sessionId) {
|
|
171
242
|
return `${this.KEY_PREFIX}${userId}:${sessionId}`;
|
|
172
243
|
}
|
|
244
|
+
getCredentialsKey(userId, sessionId) {
|
|
245
|
+
return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
|
|
246
|
+
}
|
|
173
247
|
/**
|
|
174
248
|
* Generates Redis key for tracking all sessions for a user
|
|
175
249
|
* @private
|
|
@@ -207,15 +281,16 @@ var RedisStorageBackend = class {
|
|
|
207
281
|
generateSessionId() {
|
|
208
282
|
return generateSessionId();
|
|
209
283
|
}
|
|
210
|
-
async create(session
|
|
284
|
+
async create(session) {
|
|
211
285
|
const { sessionId, userId } = session;
|
|
212
286
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
213
287
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
214
288
|
const userIdKey = this.getUserIdKey(userId);
|
|
215
|
-
const
|
|
289
|
+
const sessionWithLifecycle = normalizeNewSession(session);
|
|
290
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(sessionWithLifecycle);
|
|
216
291
|
const result = await this.redis.set(
|
|
217
292
|
sessionKey,
|
|
218
|
-
JSON.stringify(
|
|
293
|
+
JSON.stringify(sessionWithLifecycle),
|
|
219
294
|
"EX",
|
|
220
295
|
effectiveTtl,
|
|
221
296
|
"NX"
|
|
@@ -225,9 +300,8 @@ var RedisStorageBackend = class {
|
|
|
225
300
|
}
|
|
226
301
|
await this.redis.sadd(userIdKey, sessionId);
|
|
227
302
|
}
|
|
228
|
-
async update(userId, sessionId, data
|
|
303
|
+
async update(userId, sessionId, data) {
|
|
229
304
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
230
|
-
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
231
305
|
const script = `
|
|
232
306
|
local currentStr = redis.call("GET", KEYS[1])
|
|
233
307
|
if not currentStr then
|
|
@@ -235,26 +309,47 @@ var RedisStorageBackend = class {
|
|
|
235
309
|
end
|
|
236
310
|
|
|
237
311
|
local current = cjson.decode(currentStr)
|
|
238
|
-
local
|
|
239
|
-
|
|
240
|
-
for k,v in pairs(updates) do
|
|
241
|
-
current[k] = v
|
|
242
|
-
end
|
|
312
|
+
local updated = cjson.decode(ARGV[1])
|
|
243
313
|
|
|
244
|
-
redis.call("SET", KEYS[1], cjson.encode(
|
|
314
|
+
redis.call("SET", KEYS[1], cjson.encode(updated), "EX", ARGV[2])
|
|
245
315
|
return 1
|
|
246
316
|
`;
|
|
317
|
+
const current = await this.get(userId, sessionId);
|
|
318
|
+
if (!current) {
|
|
319
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
320
|
+
}
|
|
321
|
+
const updated = mergeSessionUpdate(current, data);
|
|
322
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
|
|
247
323
|
const result = await this.redis.eval(
|
|
248
324
|
script,
|
|
249
325
|
1,
|
|
250
326
|
sessionKey,
|
|
251
|
-
JSON.stringify(
|
|
327
|
+
JSON.stringify(updated),
|
|
252
328
|
effectiveTtl
|
|
253
329
|
);
|
|
254
330
|
if (result === 0) {
|
|
255
331
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
256
332
|
}
|
|
257
333
|
}
|
|
334
|
+
async patchCredentials(userId, sessionId, data) {
|
|
335
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
336
|
+
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
337
|
+
const sessionExists = await this.redis.exists(sessionKey);
|
|
338
|
+
if (!sessionExists) {
|
|
339
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
340
|
+
}
|
|
341
|
+
const session = await this.get(userId, sessionId);
|
|
342
|
+
const currentTtl = await this.redis.ttl(sessionKey);
|
|
343
|
+
const effectiveTtl = currentTtl > 0 && session ? currentTtl : resolveSessionRedisTtlSeconds(session ?? { status: "pending" });
|
|
344
|
+
const currentStr = await this.redis.get(credentialsKey);
|
|
345
|
+
const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
|
|
346
|
+
const credentials = { ...current, ...data, sessionId, userId };
|
|
347
|
+
if (effectiveTtl > 0) {
|
|
348
|
+
await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
|
|
349
|
+
} else {
|
|
350
|
+
await this.redis.set(credentialsKey, JSON.stringify(credentials));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
258
353
|
async get(userId, sessionId) {
|
|
259
354
|
try {
|
|
260
355
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
@@ -262,13 +357,28 @@ var RedisStorageBackend = class {
|
|
|
262
357
|
if (!sessionDataStr) {
|
|
263
358
|
return null;
|
|
264
359
|
}
|
|
265
|
-
const
|
|
266
|
-
return
|
|
360
|
+
const session = JSON.parse(sessionDataStr);
|
|
361
|
+
return normalizeStoredSession(session);
|
|
267
362
|
} catch (error) {
|
|
268
363
|
console.error("[RedisStorageBackend] Failed to get session:", error);
|
|
269
364
|
return null;
|
|
270
365
|
}
|
|
271
366
|
}
|
|
367
|
+
async getCredentials(userId, sessionId) {
|
|
368
|
+
const session = await this.get(userId, sessionId);
|
|
369
|
+
if (!session) return null;
|
|
370
|
+
const credentialsStr = await this.redis.get(this.getCredentialsKey(userId, sessionId));
|
|
371
|
+
return credentialsStr ? JSON.parse(credentialsStr) : { sessionId, userId };
|
|
372
|
+
}
|
|
373
|
+
async clearCredentials(userId, sessionId) {
|
|
374
|
+
await this.patchCredentials(userId, sessionId, {
|
|
375
|
+
clientInformation: null,
|
|
376
|
+
tokens: null,
|
|
377
|
+
codeVerifier: null,
|
|
378
|
+
clientId: null,
|
|
379
|
+
oauthState: null
|
|
380
|
+
});
|
|
381
|
+
}
|
|
272
382
|
async listIds(userId) {
|
|
273
383
|
const sessions2 = await this.list(userId);
|
|
274
384
|
return sessions2.map((session) => session.sessionId);
|
|
@@ -288,7 +398,7 @@ var RedisStorageBackend = class {
|
|
|
288
398
|
if (staleSessionIds.length > 0) {
|
|
289
399
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
290
400
|
}
|
|
291
|
-
return results.filter((session) => session !== null);
|
|
401
|
+
return results.filter((session) => session !== null).map((session) => normalizeStoredSession(session));
|
|
292
402
|
} catch (error) {
|
|
293
403
|
console.error(`[RedisStorageBackend] Failed to get session data for ${userId}:`, error);
|
|
294
404
|
return [];
|
|
@@ -297,9 +407,10 @@ var RedisStorageBackend = class {
|
|
|
297
407
|
async delete(userId, sessionId) {
|
|
298
408
|
try {
|
|
299
409
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
410
|
+
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
300
411
|
const userIdKey = this.getUserIdKey(userId);
|
|
301
412
|
await this.redis.srem(userIdKey, sessionId);
|
|
302
|
-
await this.redis.del(sessionKey);
|
|
413
|
+
await this.redis.del(sessionKey, credentialsKey);
|
|
303
414
|
} catch (error) {
|
|
304
415
|
console.error("[RedisStorageBackend] Failed to remove session:", error);
|
|
305
416
|
}
|
|
@@ -314,7 +425,7 @@ var RedisStorageBackend = class {
|
|
|
314
425
|
return null;
|
|
315
426
|
}
|
|
316
427
|
try {
|
|
317
|
-
return JSON.parse(data).sessionId;
|
|
428
|
+
return normalizeStoredSession(JSON.parse(data)).sessionId;
|
|
318
429
|
} catch (error) {
|
|
319
430
|
console.error("[RedisStorageBackend] Failed to parse session while listing all session IDs:", error);
|
|
320
431
|
return null;
|
|
@@ -330,8 +441,9 @@ var RedisStorageBackend = class {
|
|
|
330
441
|
async clearAll() {
|
|
331
442
|
try {
|
|
332
443
|
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
444
|
+
const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
|
|
333
445
|
const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
|
|
334
|
-
const allKeys = [...keys, ...userIdKeys];
|
|
446
|
+
const allKeys = [...keys, ...credentialKeys, ...userIdKeys];
|
|
335
447
|
if (allKeys.length > 0) {
|
|
336
448
|
await this.redis.del(...allKeys);
|
|
337
449
|
}
|
|
@@ -355,6 +467,7 @@ var RedisStorageBackend = class {
|
|
|
355
467
|
const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
|
|
356
468
|
if (staleSessionIds.length > 0) {
|
|
357
469
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
470
|
+
await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
|
|
358
471
|
}
|
|
359
472
|
const remainingCount = await this.redis.scard(userIdKey);
|
|
360
473
|
if (remainingCount === 0) {
|
|
@@ -379,6 +492,7 @@ var MemoryStorageBackend = class {
|
|
|
379
492
|
constructor() {
|
|
380
493
|
// Map<userId:sessionId, Session>
|
|
381
494
|
__publicField(this, "sessions", /* @__PURE__ */ new Map());
|
|
495
|
+
__publicField(this, "credentials", /* @__PURE__ */ new Map());
|
|
382
496
|
// Map<userId, Set<sessionId>>
|
|
383
497
|
__publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
|
|
384
498
|
}
|
|
@@ -391,36 +505,55 @@ var MemoryStorageBackend = class {
|
|
|
391
505
|
generateSessionId() {
|
|
392
506
|
return generateSessionId();
|
|
393
507
|
}
|
|
394
|
-
async create(session
|
|
508
|
+
async create(session) {
|
|
395
509
|
const { sessionId, userId } = session;
|
|
396
510
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
397
511
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
398
512
|
if (this.sessions.has(sessionKey)) {
|
|
399
513
|
throw new Error(`Session ${sessionId} already exists`);
|
|
400
514
|
}
|
|
401
|
-
this.sessions.set(sessionKey, session);
|
|
515
|
+
this.sessions.set(sessionKey, normalizeNewSession(session));
|
|
402
516
|
if (!this.userIdSessions.has(userId)) {
|
|
403
517
|
this.userIdSessions.set(userId, /* @__PURE__ */ new Set());
|
|
404
518
|
}
|
|
405
519
|
this.userIdSessions.get(userId).add(sessionId);
|
|
406
520
|
}
|
|
407
|
-
async update(userId, sessionId, data
|
|
521
|
+
async update(userId, sessionId, data) {
|
|
408
522
|
if (!userId || !sessionId) throw new Error("userId and sessionId required");
|
|
409
523
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
410
524
|
const current = this.sessions.get(sessionKey);
|
|
411
525
|
if (!current) {
|
|
412
526
|
throw new Error(`Session ${sessionId} not found`);
|
|
413
527
|
}
|
|
414
|
-
const updated =
|
|
415
|
-
...current,
|
|
416
|
-
...data
|
|
417
|
-
};
|
|
528
|
+
const updated = mergeSessionUpdate(current, data);
|
|
418
529
|
this.sessions.set(sessionKey, updated);
|
|
419
530
|
}
|
|
531
|
+
async patchCredentials(userId, sessionId, data) {
|
|
532
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
533
|
+
if (!this.sessions.has(sessionKey)) {
|
|
534
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
535
|
+
}
|
|
536
|
+
const current = this.credentials.get(sessionKey) ?? { sessionId, userId };
|
|
537
|
+
this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
538
|
+
}
|
|
420
539
|
async get(userId, sessionId) {
|
|
421
540
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
422
541
|
return this.sessions.get(sessionKey) || null;
|
|
423
542
|
}
|
|
543
|
+
async getCredentials(userId, sessionId) {
|
|
544
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
545
|
+
if (!this.sessions.has(sessionKey)) return null;
|
|
546
|
+
return this.credentials.get(sessionKey) ?? { sessionId, userId };
|
|
547
|
+
}
|
|
548
|
+
async clearCredentials(userId, sessionId) {
|
|
549
|
+
await this.patchCredentials(userId, sessionId, {
|
|
550
|
+
clientInformation: null,
|
|
551
|
+
tokens: null,
|
|
552
|
+
codeVerifier: null,
|
|
553
|
+
clientId: null,
|
|
554
|
+
oauthState: null
|
|
555
|
+
});
|
|
556
|
+
}
|
|
424
557
|
async listIds(userId) {
|
|
425
558
|
const set = this.userIdSessions.get(userId);
|
|
426
559
|
return set ? Array.from(set) : [];
|
|
@@ -440,6 +573,7 @@ var MemoryStorageBackend = class {
|
|
|
440
573
|
async delete(userId, sessionId) {
|
|
441
574
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
442
575
|
this.sessions.delete(sessionKey);
|
|
576
|
+
this.credentials.delete(sessionKey);
|
|
443
577
|
const set = this.userIdSessions.get(userId);
|
|
444
578
|
if (set) {
|
|
445
579
|
set.delete(sessionId);
|
|
@@ -453,9 +587,22 @@ var MemoryStorageBackend = class {
|
|
|
453
587
|
}
|
|
454
588
|
async clearAll() {
|
|
455
589
|
this.sessions.clear();
|
|
590
|
+
this.credentials.clear();
|
|
456
591
|
this.userIdSessions.clear();
|
|
457
592
|
}
|
|
458
593
|
async cleanupExpired() {
|
|
594
|
+
for (const [key, session] of this.sessions.entries()) {
|
|
595
|
+
if (!isSessionExpired(session)) continue;
|
|
596
|
+
this.sessions.delete(key);
|
|
597
|
+
this.credentials.delete(key);
|
|
598
|
+
const set = this.userIdSessions.get(session.userId);
|
|
599
|
+
if (set) {
|
|
600
|
+
set.delete(session.sessionId);
|
|
601
|
+
if (set.size === 0) {
|
|
602
|
+
this.userIdSessions.delete(session.userId);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
459
606
|
}
|
|
460
607
|
async disconnect() {
|
|
461
608
|
}
|
|
@@ -467,6 +614,7 @@ var FileStorageBackend = class {
|
|
|
467
614
|
constructor(options = {}) {
|
|
468
615
|
__publicField(this, "filePath");
|
|
469
616
|
__publicField(this, "memoryCache", null);
|
|
617
|
+
__publicField(this, "credentialsCache", null);
|
|
470
618
|
__publicField(this, "initialized", false);
|
|
471
619
|
this.filePath = options.path || "./sessions.json";
|
|
472
620
|
}
|
|
@@ -481,14 +629,22 @@ var FileStorageBackend = class {
|
|
|
481
629
|
const data = await promises.readFile(this.filePath, "utf-8");
|
|
482
630
|
const json = JSON.parse(data);
|
|
483
631
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
632
|
+
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
633
|
+
if (Array.isArray(json.sessions)) {
|
|
634
|
+
json.sessions.forEach((s) => {
|
|
635
|
+
const session = normalizeStoredSession(s);
|
|
636
|
+
this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
if (Array.isArray(json.credentials)) {
|
|
640
|
+
json.credentials.forEach((c) => {
|
|
641
|
+
this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
|
|
487
642
|
});
|
|
488
643
|
}
|
|
489
644
|
} catch (error) {
|
|
490
645
|
if (error.code === "ENOENT") {
|
|
491
646
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
647
|
+
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
492
648
|
await this.flush();
|
|
493
649
|
} else {
|
|
494
650
|
console.error("[FileStorage] Failed to load sessions:", error);
|
|
@@ -502,9 +658,11 @@ var FileStorageBackend = class {
|
|
|
502
658
|
if (!this.initialized) await this.init();
|
|
503
659
|
}
|
|
504
660
|
async flush() {
|
|
505
|
-
if (!this.memoryCache) return;
|
|
506
|
-
|
|
507
|
-
|
|
661
|
+
if (!this.memoryCache || !this.credentialsCache) return;
|
|
662
|
+
await promises.writeFile(this.filePath, JSON.stringify({
|
|
663
|
+
sessions: Array.from(this.memoryCache.values()),
|
|
664
|
+
credentials: Array.from(this.credentialsCache.values())
|
|
665
|
+
}, null, 2), "utf-8");
|
|
508
666
|
}
|
|
509
667
|
getSessionKey(userId, sessionId) {
|
|
510
668
|
return `${userId}:${sessionId}`;
|
|
@@ -512,7 +670,7 @@ var FileStorageBackend = class {
|
|
|
512
670
|
generateSessionId() {
|
|
513
671
|
return generateSessionId();
|
|
514
672
|
}
|
|
515
|
-
async create(session
|
|
673
|
+
async create(session) {
|
|
516
674
|
await this.ensureInitialized();
|
|
517
675
|
const { sessionId, userId } = session;
|
|
518
676
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
@@ -520,10 +678,10 @@ var FileStorageBackend = class {
|
|
|
520
678
|
if (this.memoryCache.has(sessionKey)) {
|
|
521
679
|
throw new Error(`Session ${sessionId} already exists`);
|
|
522
680
|
}
|
|
523
|
-
this.memoryCache.set(sessionKey, session);
|
|
681
|
+
this.memoryCache.set(sessionKey, normalizeNewSession(session));
|
|
524
682
|
await this.flush();
|
|
525
683
|
}
|
|
526
|
-
async update(userId, sessionId, data
|
|
684
|
+
async update(userId, sessionId, data) {
|
|
527
685
|
await this.ensureInitialized();
|
|
528
686
|
if (!userId || !sessionId) throw new Error("userId and sessionId required");
|
|
529
687
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
@@ -531,18 +689,40 @@ var FileStorageBackend = class {
|
|
|
531
689
|
if (!current) {
|
|
532
690
|
throw new Error(`Session ${sessionId} not found`);
|
|
533
691
|
}
|
|
534
|
-
const updated =
|
|
535
|
-
...current,
|
|
536
|
-
...data
|
|
537
|
-
};
|
|
692
|
+
const updated = mergeSessionUpdate(current, data);
|
|
538
693
|
this.memoryCache.set(sessionKey, updated);
|
|
539
694
|
await this.flush();
|
|
540
695
|
}
|
|
696
|
+
async patchCredentials(userId, sessionId, data) {
|
|
697
|
+
await this.ensureInitialized();
|
|
698
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
699
|
+
if (!this.memoryCache.has(sessionKey)) {
|
|
700
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
701
|
+
}
|
|
702
|
+
const current = this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
|
|
703
|
+
this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
704
|
+
await this.flush();
|
|
705
|
+
}
|
|
541
706
|
async get(userId, sessionId) {
|
|
542
707
|
await this.ensureInitialized();
|
|
543
708
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
544
709
|
return this.memoryCache.get(sessionKey) || null;
|
|
545
710
|
}
|
|
711
|
+
async getCredentials(userId, sessionId) {
|
|
712
|
+
await this.ensureInitialized();
|
|
713
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
714
|
+
if (!this.memoryCache.has(sessionKey)) return null;
|
|
715
|
+
return this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
|
|
716
|
+
}
|
|
717
|
+
async clearCredentials(userId, sessionId) {
|
|
718
|
+
await this.patchCredentials(userId, sessionId, {
|
|
719
|
+
clientInformation: null,
|
|
720
|
+
tokens: null,
|
|
721
|
+
codeVerifier: null,
|
|
722
|
+
clientId: null,
|
|
723
|
+
oauthState: null
|
|
724
|
+
});
|
|
725
|
+
}
|
|
546
726
|
async list(userId) {
|
|
547
727
|
await this.ensureInitialized();
|
|
548
728
|
return Array.from(this.memoryCache.values()).filter((s) => s.userId === userId);
|
|
@@ -554,7 +734,9 @@ var FileStorageBackend = class {
|
|
|
554
734
|
async delete(userId, sessionId) {
|
|
555
735
|
await this.ensureInitialized();
|
|
556
736
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
557
|
-
|
|
737
|
+
const deleted = this.memoryCache.delete(sessionKey);
|
|
738
|
+
this.credentialsCache.delete(sessionKey);
|
|
739
|
+
if (deleted) {
|
|
558
740
|
await this.flush();
|
|
559
741
|
}
|
|
560
742
|
}
|
|
@@ -565,10 +747,21 @@ var FileStorageBackend = class {
|
|
|
565
747
|
async clearAll() {
|
|
566
748
|
await this.ensureInitialized();
|
|
567
749
|
this.memoryCache.clear();
|
|
750
|
+
this.credentialsCache.clear();
|
|
568
751
|
await this.flush();
|
|
569
752
|
}
|
|
570
753
|
async cleanupExpired() {
|
|
571
754
|
await this.ensureInitialized();
|
|
755
|
+
let changed = false;
|
|
756
|
+
for (const [key, session] of this.memoryCache.entries()) {
|
|
757
|
+
if (!isSessionExpired(session)) continue;
|
|
758
|
+
this.memoryCache.delete(key);
|
|
759
|
+
this.credentialsCache.delete(key);
|
|
760
|
+
changed = true;
|
|
761
|
+
}
|
|
762
|
+
if (changed) {
|
|
763
|
+
await this.flush();
|
|
764
|
+
}
|
|
572
765
|
}
|
|
573
766
|
async disconnect() {
|
|
574
767
|
}
|
|
@@ -577,10 +770,12 @@ var SqliteStorage = class {
|
|
|
577
770
|
constructor(options = {}) {
|
|
578
771
|
__publicField(this, "db", null);
|
|
579
772
|
__publicField(this, "table");
|
|
773
|
+
__publicField(this, "credentialsTable");
|
|
580
774
|
__publicField(this, "initialized", false);
|
|
581
775
|
__publicField(this, "dbPath");
|
|
582
776
|
this.dbPath = options.path || "./sessions.db";
|
|
583
777
|
this.table = options.table || "mcp_sessions";
|
|
778
|
+
this.credentialsTable = `${this.table}_credentials`;
|
|
584
779
|
}
|
|
585
780
|
async init() {
|
|
586
781
|
if (this.initialized) return;
|
|
@@ -591,6 +786,7 @@ var SqliteStorage = class {
|
|
|
591
786
|
fs2.mkdirSync(dir, { recursive: true });
|
|
592
787
|
}
|
|
593
788
|
this.db = new DatabaseConstructor(this.dbPath);
|
|
789
|
+
this.db.pragma("foreign_keys = ON");
|
|
594
790
|
this.db.exec(`
|
|
595
791
|
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
596
792
|
sessionId TEXT PRIMARY KEY,
|
|
@@ -599,6 +795,13 @@ var SqliteStorage = class {
|
|
|
599
795
|
expiresAt INTEGER
|
|
600
796
|
);
|
|
601
797
|
CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
|
|
798
|
+
CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
|
|
799
|
+
sessionId TEXT NOT NULL,
|
|
800
|
+
userId TEXT NOT NULL,
|
|
801
|
+
data TEXT NOT NULL,
|
|
802
|
+
PRIMARY KEY (userId, sessionId),
|
|
803
|
+
FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
|
|
804
|
+
);
|
|
602
805
|
`);
|
|
603
806
|
this.initialized = true;
|
|
604
807
|
console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
|
|
@@ -619,18 +822,18 @@ var SqliteStorage = class {
|
|
|
619
822
|
generateSessionId() {
|
|
620
823
|
return generateSessionId();
|
|
621
824
|
}
|
|
622
|
-
async create(session
|
|
825
|
+
async create(session) {
|
|
623
826
|
this.ensureInitialized();
|
|
624
827
|
const { sessionId, userId } = session;
|
|
625
828
|
if (!sessionId || !userId) {
|
|
626
829
|
throw new Error("userId and sessionId required");
|
|
627
830
|
}
|
|
628
|
-
const
|
|
831
|
+
const sessionWithLifecycle = normalizeNewSession(session);
|
|
629
832
|
try {
|
|
630
833
|
const stmt = this.db.prepare(
|
|
631
834
|
`INSERT INTO ${this.table} (sessionId, userId, data, expiresAt) VALUES (?, ?, ?, ?)`
|
|
632
835
|
);
|
|
633
|
-
stmt.run(sessionId, userId, JSON.stringify(
|
|
836
|
+
stmt.run(sessionId, userId, JSON.stringify(sessionWithLifecycle), sessionWithLifecycle.expiresAt ?? null);
|
|
634
837
|
} catch (error) {
|
|
635
838
|
if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
636
839
|
throw new Error(`Session ${sessionId} already exists`);
|
|
@@ -638,7 +841,7 @@ var SqliteStorage = class {
|
|
|
638
841
|
throw error;
|
|
639
842
|
}
|
|
640
843
|
}
|
|
641
|
-
async update(userId, sessionId, data
|
|
844
|
+
async update(userId, sessionId, data) {
|
|
642
845
|
this.ensureInitialized();
|
|
643
846
|
if (!sessionId || !userId) {
|
|
644
847
|
throw new Error("userId and sessionId required");
|
|
@@ -647,12 +850,25 @@ var SqliteStorage = class {
|
|
|
647
850
|
if (!currentSession) {
|
|
648
851
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
649
852
|
}
|
|
650
|
-
const updatedSession =
|
|
651
|
-
const expiresAt = ttl ? Date.now() + ttl * 1e3 : null;
|
|
853
|
+
const updatedSession = mergeSessionUpdate(currentSession, data);
|
|
652
854
|
const stmt = this.db.prepare(
|
|
653
855
|
`UPDATE ${this.table} SET data = ?, expiresAt = ? WHERE sessionId = ? AND userId = ?`
|
|
654
856
|
);
|
|
655
|
-
stmt.run(JSON.stringify(updatedSession), expiresAt, sessionId, userId);
|
|
857
|
+
stmt.run(JSON.stringify(updatedSession), updatedSession.expiresAt ?? null, sessionId, userId);
|
|
858
|
+
}
|
|
859
|
+
async patchCredentials(userId, sessionId, data) {
|
|
860
|
+
this.ensureInitialized();
|
|
861
|
+
if (!await this.get(userId, sessionId)) {
|
|
862
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
863
|
+
}
|
|
864
|
+
const current = await this.getCredentials(userId, sessionId) ?? { sessionId, userId };
|
|
865
|
+
const credentials = { ...current, ...data, sessionId, userId };
|
|
866
|
+
const stmt = this.db.prepare(
|
|
867
|
+
`INSERT INTO ${this.credentialsTable} (sessionId, userId, data)
|
|
868
|
+
VALUES (?, ?, ?)
|
|
869
|
+
ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
|
|
870
|
+
);
|
|
871
|
+
stmt.run(sessionId, userId, JSON.stringify(credentials));
|
|
656
872
|
}
|
|
657
873
|
async get(userId, sessionId) {
|
|
658
874
|
this.ensureInitialized();
|
|
@@ -661,7 +877,25 @@ var SqliteStorage = class {
|
|
|
661
877
|
);
|
|
662
878
|
const row = stmt.get(sessionId, userId);
|
|
663
879
|
if (!row) return null;
|
|
664
|
-
return JSON.parse(row.data);
|
|
880
|
+
return normalizeStoredSession(JSON.parse(row.data));
|
|
881
|
+
}
|
|
882
|
+
async getCredentials(userId, sessionId) {
|
|
883
|
+
this.ensureInitialized();
|
|
884
|
+
if (!await this.get(userId, sessionId)) return null;
|
|
885
|
+
const stmt = this.db.prepare(
|
|
886
|
+
`SELECT data FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
887
|
+
);
|
|
888
|
+
const row = stmt.get(sessionId, userId);
|
|
889
|
+
return row ? JSON.parse(row.data) : { sessionId, userId };
|
|
890
|
+
}
|
|
891
|
+
async clearCredentials(userId, sessionId) {
|
|
892
|
+
await this.patchCredentials(userId, sessionId, {
|
|
893
|
+
clientInformation: null,
|
|
894
|
+
tokens: null,
|
|
895
|
+
codeVerifier: null,
|
|
896
|
+
clientId: null,
|
|
897
|
+
oauthState: null
|
|
898
|
+
});
|
|
665
899
|
}
|
|
666
900
|
async list(userId) {
|
|
667
901
|
this.ensureInitialized();
|
|
@@ -669,7 +903,7 @@ var SqliteStorage = class {
|
|
|
669
903
|
`SELECT data FROM ${this.table} WHERE userId = ?`
|
|
670
904
|
);
|
|
671
905
|
const rows = stmt.all(userId);
|
|
672
|
-
return rows.map((row) => JSON.parse(row.data));
|
|
906
|
+
return rows.map((row) => normalizeStoredSession(JSON.parse(row.data)));
|
|
673
907
|
}
|
|
674
908
|
async listIds(userId) {
|
|
675
909
|
this.ensureInitialized();
|
|
@@ -684,6 +918,9 @@ var SqliteStorage = class {
|
|
|
684
918
|
const stmt = this.db.prepare(
|
|
685
919
|
`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
686
920
|
);
|
|
921
|
+
this.db.prepare(
|
|
922
|
+
`DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
923
|
+
).run(sessionId, userId);
|
|
687
924
|
stmt.run(sessionId, userId);
|
|
688
925
|
}
|
|
689
926
|
async listAllIds() {
|
|
@@ -694,16 +931,28 @@ var SqliteStorage = class {
|
|
|
694
931
|
}
|
|
695
932
|
async clearAll() {
|
|
696
933
|
this.ensureInitialized();
|
|
934
|
+
this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
|
|
697
935
|
const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
|
|
698
936
|
stmt.run();
|
|
699
937
|
}
|
|
700
938
|
async cleanupExpired() {
|
|
701
939
|
this.ensureInitialized();
|
|
702
|
-
const
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
940
|
+
const rows = this.db.prepare(`SELECT sessionId, userId, data FROM ${this.table}`).all();
|
|
941
|
+
const deleteStmt = this.db.prepare(`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`);
|
|
942
|
+
for (const row of rows) {
|
|
943
|
+
const session = normalizeStoredSession(JSON.parse(row.data));
|
|
944
|
+
if (isSessionExpired(session)) {
|
|
945
|
+
deleteStmt.run(row.sessionId, row.userId);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
this.db.prepare(
|
|
949
|
+
`DELETE FROM ${this.credentialsTable}
|
|
950
|
+
WHERE NOT EXISTS (
|
|
951
|
+
SELECT 1 FROM ${this.table}
|
|
952
|
+
WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
|
|
953
|
+
AND ${this.table}.userId = ${this.credentialsTable}.userId
|
|
954
|
+
)`
|
|
955
|
+
).run();
|
|
707
956
|
}
|
|
708
957
|
async disconnect() {
|
|
709
958
|
if (this.db) {
|
|
@@ -782,19 +1031,21 @@ function decryptObject(data) {
|
|
|
782
1031
|
var SupabaseStorageBackend = class {
|
|
783
1032
|
constructor(supabase) {
|
|
784
1033
|
this.supabase = supabase;
|
|
785
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
786
1034
|
}
|
|
787
1035
|
async init() {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1036
|
+
await this.assertTable("mcp_sessions", "session_id");
|
|
1037
|
+
await this.assertTable("mcp_credentials", "session_id");
|
|
1038
|
+
console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
|
|
1039
|
+
}
|
|
1040
|
+
async assertTable(table, column) {
|
|
1041
|
+
const { error } = await this.supabase.from(table).select(column).limit(0);
|
|
1042
|
+
if (!error) return;
|
|
1043
|
+
if (error.code === "42P01") {
|
|
1044
|
+
throw new Error(
|
|
1045
|
+
`[SupabaseStorage] Table "${table}" not found in your database. Please run "npx mcp-ts supabase-init" to set up the required storage schema.`
|
|
1046
|
+
);
|
|
796
1047
|
}
|
|
797
|
-
|
|
1048
|
+
throw new Error(`[SupabaseStorage] Initialization check failed for "${table}": ${error.message}`);
|
|
798
1049
|
}
|
|
799
1050
|
generateSessionId() {
|
|
800
1051
|
return generateSessionId();
|
|
@@ -808,37 +1059,49 @@ var SupabaseStorageBackend = class {
|
|
|
808
1059
|
transportType: row.transport_type,
|
|
809
1060
|
callbackUrl: row.callback_url,
|
|
810
1061
|
createdAt: new Date(row.created_at).getTime(),
|
|
1062
|
+
updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
|
|
1063
|
+
expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
|
|
811
1064
|
userId: row.user_id,
|
|
812
1065
|
headers: decryptObject(row.headers),
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1066
|
+
authUrl: row.auth_url,
|
|
1067
|
+
status: row.status ?? "pending"
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
mapRowToCredentials(row, userId, sessionId) {
|
|
1071
|
+
return {
|
|
1072
|
+
sessionId,
|
|
1073
|
+
userId,
|
|
1074
|
+
clientInformation: decryptObject(row?.client_information),
|
|
1075
|
+
tokens: decryptObject(row?.tokens),
|
|
1076
|
+
codeVerifier: decryptObject(row?.code_verifier),
|
|
1077
|
+
clientId: row?.client_id,
|
|
1078
|
+
oauthState: row?.oauth_state
|
|
818
1079
|
};
|
|
819
1080
|
}
|
|
820
|
-
|
|
1081
|
+
hasCredentialData(data) {
|
|
1082
|
+
return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
|
|
1083
|
+
}
|
|
1084
|
+
async create(session) {
|
|
821
1085
|
const { sessionId, userId } = session;
|
|
822
1086
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
823
|
-
const
|
|
824
|
-
const
|
|
1087
|
+
const status = session.status ?? "pending";
|
|
1088
|
+
const createdAt = new Date(session.createdAt || Date.now()).toISOString();
|
|
1089
|
+
const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
|
|
1090
|
+
const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
|
|
825
1091
|
const { error } = await this.supabase.from("mcp_sessions").insert({
|
|
826
1092
|
session_id: sessionId,
|
|
827
1093
|
user_id: userId,
|
|
828
|
-
// Maps user_id to userId to support RLS using auth.uid()
|
|
829
1094
|
server_id: session.serverId,
|
|
830
1095
|
server_name: session.serverName,
|
|
831
1096
|
server_url: session.serverUrl,
|
|
832
1097
|
transport_type: session.transportType,
|
|
833
1098
|
callback_url: session.callbackUrl,
|
|
834
|
-
created_at:
|
|
1099
|
+
created_at: createdAt,
|
|
1100
|
+
updated_at: updatedAt,
|
|
835
1101
|
headers: encryptObject(session.headers),
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
code_verifier: session.codeVerifier,
|
|
840
|
-
client_id: session.clientId,
|
|
841
|
-
expires_at: expiresAt
|
|
1102
|
+
auth_url: session.authUrl ?? null,
|
|
1103
|
+
status,
|
|
1104
|
+
expires_at: expiresAt === null ? null : new Date(expiresAt).toISOString()
|
|
842
1105
|
});
|
|
843
1106
|
if (error) {
|
|
844
1107
|
if (error.code === "23505") {
|
|
@@ -847,11 +1110,8 @@ var SupabaseStorageBackend = class {
|
|
|
847
1110
|
throw new Error(`Failed to create session in Supabase: ${error.message}`);
|
|
848
1111
|
}
|
|
849
1112
|
}
|
|
850
|
-
async update(userId, sessionId, data
|
|
851
|
-
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
852
|
-
const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
|
|
1113
|
+
async update(userId, sessionId, data) {
|
|
853
1114
|
const updateData = {
|
|
854
|
-
expires_at: expiresAt,
|
|
855
1115
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
856
1116
|
};
|
|
857
1117
|
if ("serverId" in data) updateData.server_id = data.serverId;
|
|
@@ -859,20 +1119,50 @@ var SupabaseStorageBackend = class {
|
|
|
859
1119
|
if ("serverUrl" in data) updateData.server_url = data.serverUrl;
|
|
860
1120
|
if ("transportType" in data) updateData.transport_type = data.transportType;
|
|
861
1121
|
if ("callbackUrl" in data) updateData.callback_url = data.callbackUrl;
|
|
862
|
-
if ("
|
|
1122
|
+
if ("status" in data) {
|
|
1123
|
+
const status = data.status ?? "pending";
|
|
1124
|
+
const expiresAt = resolveSessionExpiresAt(status);
|
|
1125
|
+
updateData.status = status;
|
|
1126
|
+
updateData.expires_at = expiresAt === null ? null : new Date(expiresAt).toISOString();
|
|
1127
|
+
}
|
|
863
1128
|
if ("headers" in data) updateData.headers = encryptObject(data.headers);
|
|
864
|
-
if ("
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
if (
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1129
|
+
if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
|
|
1130
|
+
const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
|
|
1131
|
+
let updatedRows = null;
|
|
1132
|
+
if (shouldUpdateSession) {
|
|
1133
|
+
const result = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId).select("id");
|
|
1134
|
+
if (result.error) {
|
|
1135
|
+
throw new Error(`Failed to update session: ${result.error.message}`);
|
|
1136
|
+
}
|
|
1137
|
+
updatedRows = result.data;
|
|
1138
|
+
} else {
|
|
1139
|
+
const result = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
|
|
1140
|
+
if (result.error) {
|
|
1141
|
+
throw new Error(`Failed to update session: ${result.error.message}`);
|
|
1142
|
+
}
|
|
1143
|
+
updatedRows = result.data;
|
|
871
1144
|
}
|
|
872
1145
|
if (!updatedRows || updatedRows.length === 0) {
|
|
873
1146
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
874
1147
|
}
|
|
875
1148
|
}
|
|
1149
|
+
async patchCredentials(userId, sessionId, data) {
|
|
1150
|
+
if (!this.hasCredentialData(data)) return;
|
|
1151
|
+
const row = {
|
|
1152
|
+
user_id: userId,
|
|
1153
|
+
session_id: sessionId,
|
|
1154
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1155
|
+
};
|
|
1156
|
+
if ("clientInformation" in data) row.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
|
|
1157
|
+
if ("tokens" in data) row.tokens = data.tokens == null ? null : encryptObject(data.tokens);
|
|
1158
|
+
if ("codeVerifier" in data) row.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
|
|
1159
|
+
if ("clientId" in data) row.client_id = data.clientId ?? null;
|
|
1160
|
+
if ("oauthState" in data) row.oauth_state = data.oauthState ?? null;
|
|
1161
|
+
const { error } = await this.supabase.from("mcp_credentials").upsert(row, { onConflict: "user_id,session_id" });
|
|
1162
|
+
if (error) {
|
|
1163
|
+
throw new Error(`Failed to update credentials: ${error.message}`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
876
1166
|
async get(userId, sessionId) {
|
|
877
1167
|
const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
878
1168
|
if (error) {
|
|
@@ -882,6 +1172,21 @@ var SupabaseStorageBackend = class {
|
|
|
882
1172
|
if (!data) return null;
|
|
883
1173
|
return this.mapRowToSessionData(data);
|
|
884
1174
|
}
|
|
1175
|
+
async getCredentials(userId, sessionId) {
|
|
1176
|
+
const { data, error } = await this.supabase.from("mcp_credentials").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
1177
|
+
if (error) {
|
|
1178
|
+
console.error("[SupabaseStorage] Failed to get credentials:", error);
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
if (data) {
|
|
1182
|
+
return this.mapRowToCredentials(data, userId, sessionId);
|
|
1183
|
+
}
|
|
1184
|
+
const { data: sessionRows, error: sessionError } = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
|
|
1185
|
+
if (sessionError || !sessionRows || sessionRows.length === 0) {
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
return { sessionId, userId };
|
|
1189
|
+
}
|
|
885
1190
|
async list(userId) {
|
|
886
1191
|
const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
|
|
887
1192
|
if (error) {
|
|
@@ -890,6 +1195,12 @@ var SupabaseStorageBackend = class {
|
|
|
890
1195
|
}
|
|
891
1196
|
return data.map((row) => this.mapRowToSessionData(row));
|
|
892
1197
|
}
|
|
1198
|
+
async clearCredentials(userId, sessionId) {
|
|
1199
|
+
const { error } = await this.supabase.from("mcp_credentials").delete().eq("user_id", userId).eq("session_id", sessionId);
|
|
1200
|
+
if (error) {
|
|
1201
|
+
throw new Error(`Failed to clear credentials: ${error.message}`);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
893
1204
|
async delete(userId, sessionId) {
|
|
894
1205
|
const { error } = await this.supabase.from("mcp_sessions").delete().eq("user_id", userId).eq("session_id", sessionId);
|
|
895
1206
|
if (error) {
|
|
@@ -913,15 +1224,24 @@ var SupabaseStorageBackend = class {
|
|
|
913
1224
|
return data.map((row) => row.session_id);
|
|
914
1225
|
}
|
|
915
1226
|
async clearAll() {
|
|
1227
|
+
const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
|
|
1228
|
+
if (credentialsError) {
|
|
1229
|
+
console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
|
|
1230
|
+
}
|
|
916
1231
|
const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
|
|
917
1232
|
if (error) {
|
|
918
1233
|
console.error("[SupabaseStorage] Failed to clear sessions:", error);
|
|
919
1234
|
}
|
|
920
1235
|
}
|
|
921
1236
|
async cleanupExpired() {
|
|
922
|
-
const { error } = await this.supabase.from("mcp_sessions").delete().lt("expires_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
923
|
-
if (
|
|
924
|
-
console.error("[SupabaseStorage] Failed to cleanup expired sessions:",
|
|
1237
|
+
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());
|
|
1238
|
+
if (transientError) {
|
|
1239
|
+
console.error("[SupabaseStorage] Failed to cleanup expired inactive sessions:", transientError);
|
|
1240
|
+
}
|
|
1241
|
+
const dormantCutoff = new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString();
|
|
1242
|
+
const { error: dormantError } = await this.supabase.from("mcp_sessions").delete().eq("status", "active").lt("updated_at", dormantCutoff);
|
|
1243
|
+
if (dormantError) {
|
|
1244
|
+
console.error("[SupabaseStorage] Failed to cleanup dormant active sessions:", dormantError);
|
|
925
1245
|
}
|
|
926
1246
|
}
|
|
927
1247
|
async disconnect() {
|
|
@@ -932,23 +1252,29 @@ var SupabaseStorageBackend = class {
|
|
|
932
1252
|
var NeonStorageBackend = class {
|
|
933
1253
|
constructor(sql, options = {}) {
|
|
934
1254
|
this.sql = sql;
|
|
935
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
936
1255
|
__publicField(this, "tableName");
|
|
1256
|
+
__publicField(this, "credentialsTableName");
|
|
937
1257
|
const schema = options.schema || "public";
|
|
938
1258
|
const table = options.table || "mcp_sessions";
|
|
1259
|
+
const credentialsTable = options.credentialsTable || "mcp_credentials";
|
|
939
1260
|
this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
|
|
1261
|
+
this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
|
|
940
1262
|
}
|
|
941
1263
|
async init() {
|
|
1264
|
+
await this.assertTable(this.tableName, "mcp_sessions");
|
|
1265
|
+
await this.assertTable(this.credentialsTableName, "mcp_credentials");
|
|
1266
|
+
console.log("[mcp-ts][Storage] Neon: storage tables verified.");
|
|
1267
|
+
}
|
|
1268
|
+
async assertTable(qualifiedName, displayName) {
|
|
942
1269
|
const [{ exists } = { exists: null }] = await this.sql.query(
|
|
943
1270
|
"SELECT to_regclass($1) AS exists",
|
|
944
|
-
[
|
|
1271
|
+
[qualifiedName.replace(/"/g, "")]
|
|
945
1272
|
);
|
|
946
1273
|
if (!exists) {
|
|
947
1274
|
throw new Error(
|
|
948
|
-
|
|
1275
|
+
`[NeonStorage] Table "${displayName}" not found in your database. Please create it using the Neon storage guide in docs/storage-backends/neon.md.`
|
|
949
1276
|
);
|
|
950
1277
|
}
|
|
951
|
-
console.log('[mcp-ts][Storage] Neon: "mcp_sessions" table verified.');
|
|
952
1278
|
}
|
|
953
1279
|
generateSessionId() {
|
|
954
1280
|
return generateSessionId();
|
|
@@ -968,20 +1294,35 @@ var NeonStorageBackend = class {
|
|
|
968
1294
|
transportType: row.transport_type,
|
|
969
1295
|
callbackUrl: row.callback_url,
|
|
970
1296
|
createdAt: new Date(row.created_at).getTime(),
|
|
1297
|
+
updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
|
|
1298
|
+
expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
|
|
971
1299
|
userId: row.user_id,
|
|
972
1300
|
headers: decryptObject(row.headers),
|
|
973
|
-
|
|
974
|
-
|
|
1301
|
+
authUrl: row.auth_url ?? void 0,
|
|
1302
|
+
status: row.status ?? "pending"
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
mapRowToCredentials(row, userId, sessionId) {
|
|
1306
|
+
return {
|
|
1307
|
+
sessionId,
|
|
1308
|
+
userId,
|
|
1309
|
+
clientInformation: decryptObject(row.client_information),
|
|
975
1310
|
tokens: decryptObject(row.tokens),
|
|
976
|
-
codeVerifier: row.code_verifier
|
|
977
|
-
clientId: row.client_id ?? void 0
|
|
1311
|
+
codeVerifier: decryptObject(row.code_verifier),
|
|
1312
|
+
clientId: row.client_id ?? void 0,
|
|
1313
|
+
oauthState: row.oauth_state
|
|
978
1314
|
};
|
|
979
1315
|
}
|
|
980
|
-
|
|
1316
|
+
hasCredentialData(data) {
|
|
1317
|
+
return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
|
|
1318
|
+
}
|
|
1319
|
+
async create(session) {
|
|
981
1320
|
const { sessionId, userId } = session;
|
|
982
1321
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
983
|
-
const
|
|
984
|
-
const
|
|
1322
|
+
const status = session.status ?? "pending";
|
|
1323
|
+
const createdAt = new Date(session.createdAt || Date.now()).toISOString();
|
|
1324
|
+
const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
|
|
1325
|
+
const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
|
|
985
1326
|
try {
|
|
986
1327
|
await this.sql.query(
|
|
987
1328
|
`INSERT INTO ${this.tableName} (
|
|
@@ -993,16 +1334,14 @@ var NeonStorageBackend = class {
|
|
|
993
1334
|
transport_type,
|
|
994
1335
|
callback_url,
|
|
995
1336
|
created_at,
|
|
1337
|
+
updated_at,
|
|
996
1338
|
headers,
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
tokens,
|
|
1000
|
-
code_verifier,
|
|
1001
|
-
client_id,
|
|
1339
|
+
auth_url,
|
|
1340
|
+
status,
|
|
1002
1341
|
expires_at
|
|
1003
1342
|
) VALUES (
|
|
1004
1343
|
$1, $2, $3, $4, $5, $6, $7, $8,
|
|
1005
|
-
$9, $10, $11, $12, $13
|
|
1344
|
+
$9, $10, $11, $12, $13
|
|
1006
1345
|
)`,
|
|
1007
1346
|
[
|
|
1008
1347
|
sessionId,
|
|
@@ -1012,14 +1351,12 @@ var NeonStorageBackend = class {
|
|
|
1012
1351
|
session.serverUrl,
|
|
1013
1352
|
session.transportType,
|
|
1014
1353
|
session.callbackUrl,
|
|
1015
|
-
|
|
1354
|
+
createdAt,
|
|
1355
|
+
updatedAt,
|
|
1016
1356
|
encryptObject(session.headers),
|
|
1017
|
-
session.
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
session.codeVerifier,
|
|
1021
|
-
session.clientId,
|
|
1022
|
-
expiresAt
|
|
1357
|
+
session.authUrl ?? null,
|
|
1358
|
+
status,
|
|
1359
|
+
expiresAt === null ? null : new Date(expiresAt).toISOString()
|
|
1023
1360
|
]
|
|
1024
1361
|
);
|
|
1025
1362
|
} catch (error) {
|
|
@@ -1029,52 +1366,86 @@ var NeonStorageBackend = class {
|
|
|
1029
1366
|
throw new Error(`Failed to create session in Neon: ${error.message}`);
|
|
1030
1367
|
}
|
|
1031
1368
|
}
|
|
1032
|
-
async update(userId, sessionId, data
|
|
1369
|
+
async update(userId, sessionId, data) {
|
|
1033
1370
|
const currentSession = await this.get(userId, sessionId);
|
|
1034
1371
|
if (!currentSession) {
|
|
1035
1372
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1036
1373
|
}
|
|
1037
1374
|
const updatedSession = { ...currentSession, ...data };
|
|
1038
|
-
const
|
|
1039
|
-
const expiresAt =
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1375
|
+
const status = updatedSession.status ?? "pending";
|
|
1376
|
+
const expiresAt = resolveSessionExpiresAt(status);
|
|
1377
|
+
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;
|
|
1378
|
+
if (shouldUpdateSession) {
|
|
1379
|
+
const updatedRows = await this.sql.query(
|
|
1380
|
+
`UPDATE ${this.tableName}
|
|
1381
|
+
SET
|
|
1382
|
+
server_id = $1,
|
|
1383
|
+
server_name = $2,
|
|
1384
|
+
server_url = $3,
|
|
1385
|
+
transport_type = $4,
|
|
1386
|
+
callback_url = $5,
|
|
1387
|
+
status = $6,
|
|
1388
|
+
headers = $7,
|
|
1389
|
+
auth_url = $8,
|
|
1390
|
+
expires_at = $9,
|
|
1391
|
+
updated_at = now()
|
|
1392
|
+
WHERE user_id = $10 AND session_id = $11
|
|
1393
|
+
RETURNING id`,
|
|
1394
|
+
[
|
|
1395
|
+
updatedSession.serverId,
|
|
1396
|
+
updatedSession.serverName,
|
|
1397
|
+
updatedSession.serverUrl,
|
|
1398
|
+
updatedSession.transportType,
|
|
1399
|
+
updatedSession.callbackUrl,
|
|
1400
|
+
status,
|
|
1401
|
+
encryptObject(updatedSession.headers),
|
|
1402
|
+
updatedSession.authUrl ?? null,
|
|
1403
|
+
expiresAt === null ? null : new Date(expiresAt).toISOString(),
|
|
1404
|
+
userId,
|
|
1405
|
+
sessionId
|
|
1406
|
+
]
|
|
1407
|
+
);
|
|
1408
|
+
if (updatedRows.length === 0) {
|
|
1409
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
async patchCredentials(userId, sessionId, data) {
|
|
1414
|
+
if (!this.hasCredentialData(data)) return;
|
|
1415
|
+
await this.sql.query(
|
|
1416
|
+
`INSERT INTO ${this.credentialsTableName} (
|
|
1417
|
+
user_id,
|
|
1418
|
+
session_id,
|
|
1419
|
+
client_information,
|
|
1420
|
+
tokens,
|
|
1421
|
+
code_verifier,
|
|
1422
|
+
client_id,
|
|
1423
|
+
oauth_state,
|
|
1424
|
+
updated_at
|
|
1425
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, now())
|
|
1426
|
+
ON CONFLICT (user_id, session_id)
|
|
1427
|
+
DO UPDATE SET
|
|
1428
|
+
client_information = CASE WHEN $8 THEN EXCLUDED.client_information ELSE ${this.credentialsTableName}.client_information END,
|
|
1429
|
+
tokens = CASE WHEN $9 THEN EXCLUDED.tokens ELSE ${this.credentialsTableName}.tokens END,
|
|
1430
|
+
code_verifier = CASE WHEN $10 THEN EXCLUDED.code_verifier ELSE ${this.credentialsTableName}.code_verifier END,
|
|
1431
|
+
client_id = CASE WHEN $11 THEN EXCLUDED.client_id ELSE ${this.credentialsTableName}.client_id END,
|
|
1432
|
+
oauth_state = CASE WHEN $12 THEN EXCLUDED.oauth_state ELSE ${this.credentialsTableName}.oauth_state END,
|
|
1433
|
+
updated_at = now()`,
|
|
1058
1434
|
[
|
|
1059
|
-
updatedSession.serverId,
|
|
1060
|
-
updatedSession.serverName,
|
|
1061
|
-
updatedSession.serverUrl,
|
|
1062
|
-
updatedSession.transportType,
|
|
1063
|
-
updatedSession.callbackUrl,
|
|
1064
|
-
updatedSession.active ?? false,
|
|
1065
|
-
encryptObject(updatedSession.headers),
|
|
1066
|
-
updatedSession.clientInformation,
|
|
1067
|
-
updatedSession.tokens === void 0 ? null : encryptObject(updatedSession.tokens),
|
|
1068
|
-
updatedSession.codeVerifier,
|
|
1069
|
-
updatedSession.clientId,
|
|
1070
|
-
expiresAt,
|
|
1071
1435
|
userId,
|
|
1072
|
-
sessionId
|
|
1436
|
+
sessionId,
|
|
1437
|
+
"clientInformation" in data ? data.clientInformation == null ? null : encryptObject(data.clientInformation) : null,
|
|
1438
|
+
"tokens" in data ? data.tokens == null ? null : encryptObject(data.tokens) : null,
|
|
1439
|
+
"codeVerifier" in data ? data.codeVerifier == null ? null : encryptObject(data.codeVerifier) : null,
|
|
1440
|
+
"clientId" in data ? data.clientId ?? null : null,
|
|
1441
|
+
"oauthState" in data ? data.oauthState ?? null : null,
|
|
1442
|
+
"clientInformation" in data,
|
|
1443
|
+
"tokens" in data,
|
|
1444
|
+
"codeVerifier" in data,
|
|
1445
|
+
"clientId" in data,
|
|
1446
|
+
"oauthState" in data
|
|
1073
1447
|
]
|
|
1074
1448
|
);
|
|
1075
|
-
if (updatedRows.length === 0) {
|
|
1076
|
-
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1077
|
-
}
|
|
1078
1449
|
}
|
|
1079
1450
|
async get(userId, sessionId) {
|
|
1080
1451
|
try {
|
|
@@ -1082,12 +1453,32 @@ var NeonStorageBackend = class {
|
|
|
1082
1453
|
`SELECT * FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1083
1454
|
[userId, sessionId]
|
|
1084
1455
|
);
|
|
1085
|
-
|
|
1456
|
+
if (!rows[0]) return null;
|
|
1457
|
+
return this.mapRowToSessionData(rows[0]);
|
|
1086
1458
|
} catch (error) {
|
|
1087
1459
|
console.error("[NeonStorage] Failed to get session:", error);
|
|
1088
1460
|
return null;
|
|
1089
1461
|
}
|
|
1090
1462
|
}
|
|
1463
|
+
async getCredentials(userId, sessionId) {
|
|
1464
|
+
try {
|
|
1465
|
+
const credentialRows = await this.sql.query(
|
|
1466
|
+
`SELECT * FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1467
|
+
[userId, sessionId]
|
|
1468
|
+
);
|
|
1469
|
+
if (credentialRows[0]) {
|
|
1470
|
+
return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
|
|
1471
|
+
}
|
|
1472
|
+
const sessionRows = await this.sql.query(
|
|
1473
|
+
`SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1474
|
+
[userId, sessionId]
|
|
1475
|
+
);
|
|
1476
|
+
return sessionRows[0] ? { sessionId, userId } : null;
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
console.error("[NeonStorage] Failed to get credentials:", error);
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1091
1482
|
async list(userId) {
|
|
1092
1483
|
try {
|
|
1093
1484
|
const rows = await this.sql.query(
|
|
@@ -1100,6 +1491,16 @@ var NeonStorageBackend = class {
|
|
|
1100
1491
|
return [];
|
|
1101
1492
|
}
|
|
1102
1493
|
}
|
|
1494
|
+
async clearCredentials(userId, sessionId) {
|
|
1495
|
+
try {
|
|
1496
|
+
await this.sql.query(
|
|
1497
|
+
`DELETE FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1498
|
+
[userId, sessionId]
|
|
1499
|
+
);
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
console.error("[NeonStorage] Failed to clear credentials:", error);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1103
1504
|
async delete(userId, sessionId) {
|
|
1104
1505
|
try {
|
|
1105
1506
|
await this.sql.query(
|
|
@@ -1135,6 +1536,7 @@ var NeonStorageBackend = class {
|
|
|
1135
1536
|
}
|
|
1136
1537
|
async clearAll() {
|
|
1137
1538
|
try {
|
|
1539
|
+
await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
|
|
1138
1540
|
await this.sql.query(`DELETE FROM ${this.tableName}`);
|
|
1139
1541
|
} catch (error) {
|
|
1140
1542
|
console.error("[NeonStorage] Failed to clear sessions:", error);
|
|
@@ -1143,9 +1545,17 @@ var NeonStorageBackend = class {
|
|
|
1143
1545
|
async cleanupExpired() {
|
|
1144
1546
|
try {
|
|
1145
1547
|
await this.sql.query(
|
|
1146
|
-
`DELETE FROM ${this.tableName}
|
|
1548
|
+
`DELETE FROM ${this.tableName}
|
|
1549
|
+
WHERE expires_at IS NOT NULL
|
|
1550
|
+
AND expires_at < $1
|
|
1551
|
+
AND status <> 'active'`,
|
|
1147
1552
|
[(/* @__PURE__ */ new Date()).toISOString()]
|
|
1148
1553
|
);
|
|
1554
|
+
await this.sql.query(
|
|
1555
|
+
`DELETE FROM ${this.tableName}
|
|
1556
|
+
WHERE status = 'active' AND updated_at < $1`,
|
|
1557
|
+
[new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString()]
|
|
1558
|
+
);
|
|
1149
1559
|
} catch (error) {
|
|
1150
1560
|
console.error("[NeonStorage] Failed to cleanup expired sessions:", error);
|
|
1151
1561
|
}
|
|
@@ -1192,26 +1602,24 @@ function emitSessionMutation(event) {
|
|
|
1192
1602
|
function createSessionMutationEvent(prop, args) {
|
|
1193
1603
|
const timestamp = Date.now();
|
|
1194
1604
|
if (prop === "create") {
|
|
1195
|
-
const [session
|
|
1605
|
+
const [session] = args;
|
|
1196
1606
|
if (!session?.userId || !session?.sessionId) return null;
|
|
1197
1607
|
return {
|
|
1198
1608
|
type: "create",
|
|
1199
1609
|
userId: session.userId,
|
|
1200
1610
|
sessionId: session.sessionId,
|
|
1201
1611
|
session,
|
|
1202
|
-
ttl,
|
|
1203
1612
|
timestamp
|
|
1204
1613
|
};
|
|
1205
1614
|
}
|
|
1206
1615
|
if (prop === "update") {
|
|
1207
|
-
const [userId, sessionId, patch
|
|
1616
|
+
const [userId, sessionId, patch] = args;
|
|
1208
1617
|
if (!userId || !sessionId) return null;
|
|
1209
1618
|
return {
|
|
1210
1619
|
type: "update",
|
|
1211
1620
|
userId,
|
|
1212
1621
|
sessionId,
|
|
1213
1622
|
patch,
|
|
1214
|
-
ttl,
|
|
1215
1623
|
timestamp
|
|
1216
1624
|
};
|
|
1217
1625
|
}
|
|
@@ -1407,7 +1815,6 @@ var StorageOAuthClientProvider = class {
|
|
|
1407
1815
|
__publicField(this, "_authUrl");
|
|
1408
1816
|
__publicField(this, "_clientId");
|
|
1409
1817
|
__publicField(this, "onRedirectCallback");
|
|
1410
|
-
__publicField(this, "tokenExpiresAt");
|
|
1411
1818
|
this.userId = options.userId;
|
|
1412
1819
|
this.serverId = options.serverId;
|
|
1413
1820
|
this.sessionId = options.sessionId;
|
|
@@ -1441,30 +1848,30 @@ var StorageOAuthClientProvider = class {
|
|
|
1441
1848
|
this._clientId = clientId_;
|
|
1442
1849
|
}
|
|
1443
1850
|
/**
|
|
1444
|
-
* Loads OAuth
|
|
1851
|
+
* Loads OAuth credentials from the session store
|
|
1445
1852
|
* @private
|
|
1446
1853
|
*/
|
|
1447
|
-
async
|
|
1448
|
-
const data = await sessions.
|
|
1854
|
+
async getCredentials() {
|
|
1855
|
+
const data = await sessions.getCredentials(this.userId, this.sessionId);
|
|
1449
1856
|
if (!data) {
|
|
1450
|
-
return {};
|
|
1857
|
+
return { userId: this.userId, sessionId: this.sessionId };
|
|
1451
1858
|
}
|
|
1452
1859
|
return data;
|
|
1453
1860
|
}
|
|
1454
1861
|
/**
|
|
1455
|
-
* Saves OAuth
|
|
1456
|
-
* @param data - Partial OAuth
|
|
1862
|
+
* Saves OAuth credentials to the session store
|
|
1863
|
+
* @param data - Partial OAuth credentials to save
|
|
1457
1864
|
* @private
|
|
1458
1865
|
* @throws Error if session doesn't exist (session must be created by controller layer)
|
|
1459
1866
|
*/
|
|
1460
|
-
async
|
|
1461
|
-
await sessions.
|
|
1867
|
+
async patchCredentials(data) {
|
|
1868
|
+
await sessions.patchCredentials(this.userId, this.sessionId, data);
|
|
1462
1869
|
}
|
|
1463
1870
|
/**
|
|
1464
1871
|
* Retrieves stored OAuth client information
|
|
1465
1872
|
*/
|
|
1466
1873
|
async clientInformation() {
|
|
1467
|
-
const data = await this.
|
|
1874
|
+
const data = await this.getCredentials();
|
|
1468
1875
|
if (data.clientId && !this._clientId) {
|
|
1469
1876
|
this._clientId = data.clientId;
|
|
1470
1877
|
}
|
|
@@ -1483,7 +1890,7 @@ var StorageOAuthClientProvider = class {
|
|
|
1483
1890
|
* Stores OAuth client information
|
|
1484
1891
|
*/
|
|
1485
1892
|
async saveClientInformation(clientInformation) {
|
|
1486
|
-
await this.
|
|
1893
|
+
await this.patchCredentials({
|
|
1487
1894
|
clientInformation,
|
|
1488
1895
|
clientId: clientInformation.client_id
|
|
1489
1896
|
});
|
|
@@ -1493,29 +1900,58 @@ var StorageOAuthClientProvider = class {
|
|
|
1493
1900
|
* Stores OAuth tokens
|
|
1494
1901
|
*/
|
|
1495
1902
|
async saveTokens(tokens) {
|
|
1496
|
-
|
|
1497
|
-
if (tokens.expires_in) {
|
|
1498
|
-
this.tokenExpiresAt = Date.now() + tokens.expires_in * 1e3 - TOKEN_EXPIRY_BUFFER_MS;
|
|
1499
|
-
}
|
|
1500
|
-
await this.saveSessionData(data);
|
|
1903
|
+
await this.patchCredentials({ tokens });
|
|
1501
1904
|
}
|
|
1502
1905
|
get authUrl() {
|
|
1503
1906
|
return this._authUrl;
|
|
1504
1907
|
}
|
|
1505
1908
|
async state() {
|
|
1506
|
-
|
|
1909
|
+
const nonce = nanoid(32);
|
|
1910
|
+
await this.patchCredentials({
|
|
1911
|
+
oauthState: {
|
|
1912
|
+
nonce,
|
|
1913
|
+
sessionId: this.sessionId,
|
|
1914
|
+
serverId: this.serverId,
|
|
1915
|
+
createdAt: Date.now()
|
|
1916
|
+
},
|
|
1917
|
+
codeVerifier: null
|
|
1918
|
+
});
|
|
1919
|
+
return formatOAuthState(nonce, this.sessionId);
|
|
1507
1920
|
}
|
|
1508
|
-
async checkState(
|
|
1509
|
-
const
|
|
1921
|
+
async checkState(state) {
|
|
1922
|
+
const parsed = parseOAuthState(state);
|
|
1923
|
+
if (!parsed) {
|
|
1924
|
+
return { valid: false, error: "Invalid OAuth state" };
|
|
1925
|
+
}
|
|
1926
|
+
if (parsed.sessionId !== this.sessionId) {
|
|
1927
|
+
return { valid: false, error: "OAuth state mismatch" };
|
|
1928
|
+
}
|
|
1929
|
+
const data = await sessions.getCredentials(this.userId, parsed.sessionId);
|
|
1510
1930
|
if (!data) {
|
|
1511
1931
|
return { valid: false, error: "Session not found" };
|
|
1512
1932
|
}
|
|
1513
|
-
|
|
1933
|
+
const oauthState = data.oauthState;
|
|
1934
|
+
if (!oauthState) {
|
|
1935
|
+
return { valid: false, error: "OAuth state not found" };
|
|
1936
|
+
}
|
|
1937
|
+
if (oauthState.nonce !== parsed.nonce || oauthState.sessionId !== parsed.sessionId || oauthState.serverId !== this.serverId) {
|
|
1938
|
+
return { valid: false, error: "OAuth state mismatch" };
|
|
1939
|
+
}
|
|
1940
|
+
if (Date.now() - oauthState.createdAt > STATE_EXPIRATION_MS) {
|
|
1941
|
+
return { valid: false, error: "OAuth state expired" };
|
|
1942
|
+
}
|
|
1943
|
+
return { valid: true, serverId: oauthState.serverId };
|
|
1514
1944
|
}
|
|
1515
|
-
async consumeState(
|
|
1945
|
+
async consumeState(state) {
|
|
1946
|
+
const result = await this.checkState(state);
|
|
1947
|
+
if (!result.valid) {
|
|
1948
|
+
throw new Error(result.error || "Invalid OAuth state");
|
|
1949
|
+
}
|
|
1950
|
+
await this.patchCredentials({ oauthState: null });
|
|
1516
1951
|
}
|
|
1517
1952
|
async redirectToAuthorization(authUrl) {
|
|
1518
1953
|
this._authUrl = authUrl.toString();
|
|
1954
|
+
await sessions.update(this.userId, this.sessionId, { authUrl: this._authUrl });
|
|
1519
1955
|
if (this.onRedirectCallback) {
|
|
1520
1956
|
this.onRedirectCallback(authUrl.toString());
|
|
1521
1957
|
}
|
|
@@ -1526,21 +1962,25 @@ var StorageOAuthClientProvider = class {
|
|
|
1526
1962
|
} else {
|
|
1527
1963
|
const updates = {};
|
|
1528
1964
|
if (scope === "client") {
|
|
1529
|
-
updates.clientInformation =
|
|
1530
|
-
updates.clientId =
|
|
1965
|
+
updates.clientInformation = null;
|
|
1966
|
+
updates.clientId = null;
|
|
1531
1967
|
} else if (scope === "tokens") {
|
|
1532
|
-
updates.tokens =
|
|
1968
|
+
updates.tokens = null;
|
|
1533
1969
|
} else if (scope === "verifier") {
|
|
1534
|
-
updates.codeVerifier =
|
|
1970
|
+
updates.codeVerifier = null;
|
|
1535
1971
|
}
|
|
1536
|
-
await this.
|
|
1972
|
+
await this.patchCredentials(updates);
|
|
1537
1973
|
}
|
|
1538
1974
|
}
|
|
1539
1975
|
async saveCodeVerifier(verifier) {
|
|
1540
|
-
await this.
|
|
1976
|
+
const data = await this.getCredentials();
|
|
1977
|
+
if (data.codeVerifier) {
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
await this.patchCredentials({ codeVerifier: verifier });
|
|
1541
1981
|
}
|
|
1542
1982
|
async codeVerifier() {
|
|
1543
|
-
const data = await this.
|
|
1983
|
+
const data = await this.getCredentials();
|
|
1544
1984
|
if (data.clientId && !this._clientId) {
|
|
1545
1985
|
this._clientId = data.clientId;
|
|
1546
1986
|
}
|
|
@@ -1550,23 +1990,14 @@ var StorageOAuthClientProvider = class {
|
|
|
1550
1990
|
return data.codeVerifier;
|
|
1551
1991
|
}
|
|
1552
1992
|
async deleteCodeVerifier() {
|
|
1553
|
-
await this.
|
|
1993
|
+
await this.patchCredentials({ codeVerifier: null });
|
|
1554
1994
|
}
|
|
1555
1995
|
async tokens() {
|
|
1556
|
-
const data = await this.
|
|
1996
|
+
const data = await this.getCredentials();
|
|
1557
1997
|
if (data.clientId && !this._clientId) {
|
|
1558
1998
|
this._clientId = data.clientId;
|
|
1559
1999
|
}
|
|
1560
|
-
return data.tokens;
|
|
1561
|
-
}
|
|
1562
|
-
isTokenExpired() {
|
|
1563
|
-
if (!this.tokenExpiresAt) {
|
|
1564
|
-
return false;
|
|
1565
|
-
}
|
|
1566
|
-
return Date.now() >= this.tokenExpiresAt;
|
|
1567
|
-
}
|
|
1568
|
-
setTokenExpiresAt(expiresAt) {
|
|
1569
|
-
this.tokenExpiresAt = expiresAt;
|
|
2000
|
+
return data.tokens ?? void 0;
|
|
1570
2001
|
}
|
|
1571
2002
|
};
|
|
1572
2003
|
|
|
@@ -1888,17 +2319,18 @@ var MCPClient = class {
|
|
|
1888
2319
|
}
|
|
1889
2320
|
this.emitStateChange("INITIALIZING");
|
|
1890
2321
|
this.emitProgress("Loading session configuration...");
|
|
2322
|
+
let existingSession = null;
|
|
1891
2323
|
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
1892
|
-
|
|
1893
|
-
if (!
|
|
2324
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2325
|
+
if (!existingSession) {
|
|
1894
2326
|
throw new Error(`Session not found: ${this.sessionId}`);
|
|
1895
2327
|
}
|
|
1896
|
-
this.serverUrl = this.serverUrl ||
|
|
1897
|
-
this.callbackUrl = this.callbackUrl ||
|
|
1898
|
-
this.serverName = this.serverName ||
|
|
1899
|
-
this.serverId = this.serverId ||
|
|
1900
|
-
this.headers = this.headers ||
|
|
1901
|
-
this.createdAt =
|
|
2328
|
+
this.serverUrl = this.serverUrl || existingSession.serverUrl;
|
|
2329
|
+
this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
|
|
2330
|
+
this.serverName = this.serverName || existingSession.serverName;
|
|
2331
|
+
this.serverId = this.serverId || existingSession.serverId || "unknown";
|
|
2332
|
+
this.headers = this.headers || existingSession.headers;
|
|
2333
|
+
this.createdAt = existingSession.createdAt;
|
|
1902
2334
|
}
|
|
1903
2335
|
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
1904
2336
|
throw new Error("Missing required connection metadata");
|
|
@@ -1942,10 +2374,13 @@ var MCPClient = class {
|
|
|
1942
2374
|
}
|
|
1943
2375
|
);
|
|
1944
2376
|
}
|
|
1945
|
-
|
|
2377
|
+
if (existingSession === null) {
|
|
2378
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2379
|
+
}
|
|
1946
2380
|
if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
|
|
1947
2381
|
this.createdAt = Date.now();
|
|
1948
|
-
|
|
2382
|
+
const updatedAt = this.createdAt;
|
|
2383
|
+
console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
|
|
1949
2384
|
await sessions.create({
|
|
1950
2385
|
sessionId: this.sessionId,
|
|
1951
2386
|
userId: this.userId,
|
|
@@ -1956,18 +2391,18 @@ var MCPClient = class {
|
|
|
1956
2391
|
transportType: this.transportType || "streamable-http",
|
|
1957
2392
|
headers: this.headers,
|
|
1958
2393
|
createdAt: this.createdAt,
|
|
1959
|
-
|
|
1960
|
-
|
|
2394
|
+
updatedAt,
|
|
2395
|
+
status: "pending"
|
|
2396
|
+
});
|
|
1961
2397
|
}
|
|
1962
2398
|
}
|
|
1963
2399
|
/**
|
|
1964
2400
|
* Saves current session state to the session store
|
|
1965
2401
|
* Creates new session if it doesn't exist, updates if it does
|
|
1966
|
-
* @param
|
|
1967
|
-
* @param active - Session status marker used to avoid unnecessary TTL rewrites
|
|
2402
|
+
* @param status - Session lifecycle status used by storage cleanup
|
|
1968
2403
|
* @private
|
|
1969
2404
|
*/
|
|
1970
|
-
async saveSession(
|
|
2405
|
+
async saveSession(status = "active", existingSession) {
|
|
1971
2406
|
if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
|
|
1972
2407
|
return;
|
|
1973
2408
|
}
|
|
@@ -1981,13 +2416,29 @@ var MCPClient = class {
|
|
|
1981
2416
|
transportType: this.transportType || "streamable-http",
|
|
1982
2417
|
headers: this.headers,
|
|
1983
2418
|
createdAt: this.createdAt || Date.now(),
|
|
1984
|
-
|
|
2419
|
+
updatedAt: Date.now(),
|
|
2420
|
+
status
|
|
1985
2421
|
};
|
|
1986
|
-
|
|
2422
|
+
if (status === "active") {
|
|
2423
|
+
sessionData.authUrl = null;
|
|
2424
|
+
}
|
|
2425
|
+
if (existingSession === void 0) {
|
|
2426
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2427
|
+
}
|
|
1987
2428
|
if (existingSession) {
|
|
1988
|
-
await sessions.update(this.userId, this.sessionId, sessionData
|
|
2429
|
+
await sessions.update(this.userId, this.sessionId, sessionData);
|
|
1989
2430
|
} else {
|
|
1990
|
-
await sessions.create(sessionData
|
|
2431
|
+
await sessions.create(sessionData);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Removes transient setup/auth sessions without masking the original error.
|
|
2436
|
+
* @private
|
|
2437
|
+
*/
|
|
2438
|
+
async deleteTransientSession() {
|
|
2439
|
+
try {
|
|
2440
|
+
await sessions.delete(this.userId, this.sessionId);
|
|
2441
|
+
} catch {
|
|
1991
2442
|
}
|
|
1992
2443
|
}
|
|
1993
2444
|
/**
|
|
@@ -2052,8 +2503,8 @@ var MCPClient = class {
|
|
|
2052
2503
|
this.transportType = transportType;
|
|
2053
2504
|
this.emitStateChange("CONNECTED");
|
|
2054
2505
|
this.emitProgress("Connected successfully");
|
|
2055
|
-
console.log(`[MCPClient] Saving session ${this.sessionId}
|
|
2056
|
-
await this.saveSession(
|
|
2506
|
+
console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
|
|
2507
|
+
await this.saveSession("active");
|
|
2057
2508
|
} catch (error) {
|
|
2058
2509
|
if (error instanceof UnauthorizedError$1 || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
|
|
2059
2510
|
let authUrl = "";
|
|
@@ -2065,15 +2516,12 @@ var MCPClient = class {
|
|
|
2065
2516
|
const message = detail.toLowerCase() === "unauthorized" ? "OAuth authorization URL not available" : `OAuth authorization URL not available: ${detail}`;
|
|
2066
2517
|
this.emitError(message, "auth");
|
|
2067
2518
|
this.emitStateChange("FAILED");
|
|
2068
|
-
|
|
2069
|
-
await sessions.delete(this.userId, this.sessionId);
|
|
2070
|
-
} catch {
|
|
2071
|
-
}
|
|
2519
|
+
await this.deleteTransientSession();
|
|
2072
2520
|
throw new Error(message);
|
|
2073
2521
|
}
|
|
2074
2522
|
this.emitStateChange("AUTHENTICATING");
|
|
2075
|
-
console.log(`[MCPClient] Saving session ${this.sessionId}
|
|
2076
|
-
await this.saveSession(
|
|
2523
|
+
console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
|
|
2524
|
+
await this.saveSession("pending");
|
|
2077
2525
|
if (this.serverId) {
|
|
2078
2526
|
this._onConnectionEvent.fire({
|
|
2079
2527
|
type: "auth_required",
|
|
@@ -2093,7 +2541,7 @@ var MCPClient = class {
|
|
|
2093
2541
|
this.emitStateChange("FAILED");
|
|
2094
2542
|
try {
|
|
2095
2543
|
const existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2096
|
-
if (!existingSession || existingSession.
|
|
2544
|
+
if (!existingSession || existingSession.status !== "active") {
|
|
2097
2545
|
await sessions.delete(this.userId, this.sessionId);
|
|
2098
2546
|
}
|
|
2099
2547
|
} catch {
|
|
@@ -2108,7 +2556,7 @@ var MCPClient = class {
|
|
|
2108
2556
|
* @param authCode - Authorization code received from OAuth callback
|
|
2109
2557
|
*/
|
|
2110
2558
|
// TODO: needs to be optimized
|
|
2111
|
-
async finishAuth(authCode) {
|
|
2559
|
+
async finishAuth(authCode, state) {
|
|
2112
2560
|
this.emitStateChange("AUTHENTICATING");
|
|
2113
2561
|
this.emitProgress("Exchanging authorization code for tokens...");
|
|
2114
2562
|
await this.initialize();
|
|
@@ -2118,6 +2566,16 @@ var MCPClient = class {
|
|
|
2118
2566
|
this.emitStateChange("FAILED");
|
|
2119
2567
|
throw new Error(error);
|
|
2120
2568
|
}
|
|
2569
|
+
if (state) {
|
|
2570
|
+
const stateCheck = await this.oauthProvider.checkState(state);
|
|
2571
|
+
if (!stateCheck.valid) {
|
|
2572
|
+
const error = stateCheck.error || "Invalid OAuth state";
|
|
2573
|
+
this.emitError(error, "auth");
|
|
2574
|
+
this.emitStateChange("FAILED");
|
|
2575
|
+
throw new Error(error);
|
|
2576
|
+
}
|
|
2577
|
+
await this.oauthProvider.consumeState(state);
|
|
2578
|
+
}
|
|
2121
2579
|
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2122
2580
|
let lastError;
|
|
2123
2581
|
let tokensExchanged = false;
|
|
@@ -2157,8 +2615,8 @@ var MCPClient = class {
|
|
|
2157
2615
|
await this.client.connect(this.transport);
|
|
2158
2616
|
this.transportType = currentType;
|
|
2159
2617
|
this.emitStateChange("CONNECTED");
|
|
2160
|
-
console.log(`[MCPClient]
|
|
2161
|
-
await this.saveSession(
|
|
2618
|
+
console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
|
|
2619
|
+
await this.saveSession("active");
|
|
2162
2620
|
return;
|
|
2163
2621
|
} catch (error) {
|
|
2164
2622
|
lastError = error;
|
|
@@ -2171,12 +2629,14 @@ var MCPClient = class {
|
|
|
2171
2629
|
const msg = error instanceof Error ? error.message : "Authentication failed";
|
|
2172
2630
|
this.emitError(msg, "auth");
|
|
2173
2631
|
this.emitStateChange("FAILED");
|
|
2632
|
+
await this.deleteTransientSession();
|
|
2174
2633
|
throw error;
|
|
2175
2634
|
}
|
|
2176
2635
|
if (isLastAttempt) {
|
|
2177
2636
|
const msg = error instanceof Error ? error.message : "Authentication failed";
|
|
2178
2637
|
this.emitError(msg, "auth");
|
|
2179
2638
|
this.emitStateChange("FAILED");
|
|
2639
|
+
await this.deleteTransientSession();
|
|
2180
2640
|
throw error;
|
|
2181
2641
|
}
|
|
2182
2642
|
this.emitProgress(`Auth attempt with ${currentType} failed: ${errorMessage}. Retrying...`);
|
|
@@ -2186,6 +2646,7 @@ var MCPClient = class {
|
|
|
2186
2646
|
const errorMessage = lastError instanceof Error ? lastError.message : "Authentication failed";
|
|
2187
2647
|
this.emitError(errorMessage, "auth");
|
|
2188
2648
|
this.emitStateChange("FAILED");
|
|
2649
|
+
await this.deleteTransientSession();
|
|
2189
2650
|
throw lastError;
|
|
2190
2651
|
}
|
|
2191
2652
|
}
|
|
@@ -2500,48 +2961,6 @@ var MCPClient = class {
|
|
|
2500
2961
|
getSessionId() {
|
|
2501
2962
|
return this.sessionId;
|
|
2502
2963
|
}
|
|
2503
|
-
/**
|
|
2504
|
-
* Gets MCP server configuration for all active user sessions
|
|
2505
|
-
* Loads sessions from storage and returns server connection metadata.
|
|
2506
|
-
* OAuth refresh is handled by SDK transports through their authProvider.
|
|
2507
|
-
* @deprecated This returns legacy connection metadata only and does not
|
|
2508
|
-
* include OAuth tokens or generated Authorization headers. Prefer
|
|
2509
|
-
* MultiSessionClient or explicit MCPClient instances so SDK transports can
|
|
2510
|
-
* own OAuth refresh and reauthorization.
|
|
2511
|
-
* @param userId - User ID to fetch sessions for
|
|
2512
|
-
* @returns Object keyed by sanitized server labels containing transport and url.
|
|
2513
|
-
* @static
|
|
2514
|
-
*/
|
|
2515
|
-
static async getMcpServerConfig(userId) {
|
|
2516
|
-
const mcpConfig = {};
|
|
2517
|
-
const sessionList = await sessions.list(userId);
|
|
2518
|
-
await Promise.all(
|
|
2519
|
-
sessionList.map(async (sessionData) => {
|
|
2520
|
-
const { sessionId } = sessionData;
|
|
2521
|
-
try {
|
|
2522
|
-
if (!sessionData.serverId || !sessionData.transportType || !sessionData.serverUrl || !sessionData.callbackUrl) {
|
|
2523
|
-
await sessions.delete(userId, sessionId);
|
|
2524
|
-
return;
|
|
2525
|
-
}
|
|
2526
|
-
const label = sanitizeServerLabel(
|
|
2527
|
-
sessionData.serverName || sessionData.serverId || "server"
|
|
2528
|
-
);
|
|
2529
|
-
mcpConfig[label] = {
|
|
2530
|
-
transport: sessionData.transportType,
|
|
2531
|
-
url: sessionData.serverUrl,
|
|
2532
|
-
...sessionData.serverName && {
|
|
2533
|
-
serverName: sessionData.serverName,
|
|
2534
|
-
serverLabel: label
|
|
2535
|
-
}
|
|
2536
|
-
};
|
|
2537
|
-
} catch (error) {
|
|
2538
|
-
await sessions.delete(userId, sessionId);
|
|
2539
|
-
console.warn(`[MCP] Failed to process session ${sessionId}:`, error);
|
|
2540
|
-
}
|
|
2541
|
-
})
|
|
2542
|
-
);
|
|
2543
|
-
return mcpConfig;
|
|
2544
|
-
}
|
|
2545
2964
|
};
|
|
2546
2965
|
|
|
2547
2966
|
// src/server/mcp/multi-session-client.ts
|
|
@@ -2576,18 +2995,13 @@ var MultiSessionClient = class {
|
|
|
2576
2995
|
*
|
|
2577
2996
|
* A session is considered connectable when:
|
|
2578
2997
|
* - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
|
|
2579
|
-
* - Its
|
|
2580
|
-
* either mid-OAuth flow, auth-pending, or previously failed. We skip those here
|
|
2998
|
+
* - Its status is `active`. Pending sessions are skipped here
|
|
2581
2999
|
* and let the OAuth flow complete separately before we try to reconnect them.
|
|
2582
|
-
*
|
|
2583
|
-
* Note: Sessions where `active` is `undefined` (legacy records) are included
|
|
2584
|
-
* for backwards compatibility.
|
|
2585
3000
|
*/
|
|
2586
3001
|
async getActiveSessions() {
|
|
2587
3002
|
const sessionList = await sessions.list(this.userId);
|
|
2588
3003
|
const valid = sessionList.filter(
|
|
2589
|
-
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.
|
|
2590
|
-
// exclude OAuth-pending / failed sessions
|
|
3004
|
+
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
|
|
2591
3005
|
);
|
|
2592
3006
|
return valid;
|
|
2593
3007
|
}
|
|
@@ -2858,7 +3272,8 @@ var SSEConnectionManager = class {
|
|
|
2858
3272
|
serverUrl: s.serverUrl,
|
|
2859
3273
|
transport: s.transportType,
|
|
2860
3274
|
createdAt: s.createdAt,
|
|
2861
|
-
|
|
3275
|
+
updatedAt: s.updatedAt ?? s.createdAt,
|
|
3276
|
+
status: s.status ?? "pending"
|
|
2862
3277
|
}))
|
|
2863
3278
|
};
|
|
2864
3279
|
}
|
|
@@ -2874,7 +3289,7 @@ var SSEConnectionManager = class {
|
|
|
2874
3289
|
(s) => s.serverId === serverId || s.serverUrl === serverUrl
|
|
2875
3290
|
);
|
|
2876
3291
|
if (duplicate) {
|
|
2877
|
-
if (duplicate.
|
|
3292
|
+
if (duplicate.status === "pending") {
|
|
2878
3293
|
await this.getSession({ sessionId: duplicate.sessionId });
|
|
2879
3294
|
return {
|
|
2880
3295
|
sessionId: duplicate.sessionId,
|
|
@@ -3061,7 +3476,10 @@ var SSEConnectionManager = class {
|
|
|
3061
3476
|
* Complete OAuth authorization flow
|
|
3062
3477
|
*/
|
|
3063
3478
|
async finishAuth(params) {
|
|
3064
|
-
const {
|
|
3479
|
+
const { code } = params;
|
|
3480
|
+
const oauthState = params.state;
|
|
3481
|
+
const parsedState = parseOAuthState(oauthState);
|
|
3482
|
+
const sessionId = parsedState?.sessionId || oauthState;
|
|
3065
3483
|
const session = await sessions.get(this.userId, sessionId);
|
|
3066
3484
|
if (!session) {
|
|
3067
3485
|
throw new Error("Session not found");
|
|
@@ -3086,7 +3504,7 @@ var SSEConnectionManager = class {
|
|
|
3086
3504
|
headers: session.headers
|
|
3087
3505
|
});
|
|
3088
3506
|
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
3089
|
-
await client.finishAuth(code);
|
|
3507
|
+
await client.finishAuth(code, oauthState);
|
|
3090
3508
|
this.clients.set(sessionId, client);
|
|
3091
3509
|
const tools = await client.listTools();
|
|
3092
3510
|
return { success: true, toolCount: tools.tools.length };
|
|
@@ -3391,8 +3809,8 @@ var SSEClient = class {
|
|
|
3391
3809
|
async getSession(sessionId) {
|
|
3392
3810
|
return this.sendRequest("getSession", { sessionId });
|
|
3393
3811
|
}
|
|
3394
|
-
async finishAuth(
|
|
3395
|
-
return this.sendRequest("finishAuth", {
|
|
3812
|
+
async finishAuth(state, code) {
|
|
3813
|
+
return this.sendRequest("finishAuth", { state, code });
|
|
3396
3814
|
}
|
|
3397
3815
|
async listPrompts(sessionId) {
|
|
3398
3816
|
return this.sendRequest("listPrompts", { sessionId });
|
|
@@ -5246,6 +5664,6 @@ function globToRegExp(pattern) {
|
|
|
5246
5664
|
return new RegExp(regexPattern);
|
|
5247
5665
|
}
|
|
5248
5666
|
|
|
5249
|
-
export { APP_HOST_DEFAULTS, AppHost, AuthenticationError, ConfigurationError, ConnectionError, DEFAULT_CLIENT_NAME, DEFAULT_CLIENT_URI, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_LOGO_URI, DEFAULT_MCP_APP_CSP, DEFAULT_POLICY_URI, DisposableStore, Emitter, InvalidStateError, MCPClient, MCP_CLIENT_NAME, MCP_CLIENT_VERSION, McpError, MultiSessionClient, NotConnectedError, REDIS_KEY_PREFIX, RpcErrorCodes, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD,
|
|
5667
|
+
export { APP_HOST_DEFAULTS, AppHost, AuthenticationError, ConfigurationError, ConnectionError, DEFAULT_CLIENT_NAME, DEFAULT_CLIENT_URI, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_LOGO_URI, DEFAULT_MCP_APP_CSP, DEFAULT_POLICY_URI, DORMANT_SESSION_EXPIRATION_MS, DORMANT_SESSION_EXPIRATION_SECONDS, DisposableStore, Emitter, InvalidStateError, MCPClient, MCP_CLIENT_NAME, MCP_CLIENT_VERSION, McpError, MultiSessionClient, NotConnectedError, PENDING_SESSION_EXPIRATION_SECONDS, REDIS_KEY_PREFIX, RpcErrorCodes, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, SOFTWARE_ID, SOFTWARE_VERSION, SSEClient, SSEConnectionManager, STATE_EXPIRATION_MS, SchemaCompressor, SessionNotFoundError, SessionValidationError, StorageOAuthClientProvider, ToolExecutionError, ToolIndex, ToolRouter, UnauthorizedError, createExecuteToolDefinition, createGetSchemaToolDefinition, createListServersToolDefinition, createNextMcpHandler, createRegexSearchToolDefinition, createSSEHandler, createSearchToolDefinition, executeMetaTool, findToolByName, getToolUiResourceUri, isCallToolSuccess, isConnectAuthRequired, isConnectError, isConnectSuccess, isListToolsSuccess, isMetaTool, onSessionMutation, resolveMetaToolProxy, sanitizeServerLabel, sessions };
|
|
5250
5668
|
//# sourceMappingURL=index.mjs.map
|
|
5251
5669
|
//# sourceMappingURL=index.mjs.map
|