@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.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
|
2
2
|
import { customAlphabet, nanoid } from 'nanoid';
|
|
3
3
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
4
4
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
5
|
-
import { UnauthorizedError as UnauthorizedError$1
|
|
5
|
+
import { UnauthorizedError as UnauthorizedError$1 } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
6
6
|
import { ListToolsResultSchema, CallToolResultSchema, ListPromptsResultSchema, GetPromptResultSchema, ListResourcesResultSchema, ReadResourceResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
7
7
|
import * as fs2 from 'fs';
|
|
8
8
|
import { promises } from 'fs';
|
|
@@ -111,19 +111,7 @@ var init_redis = __esm({
|
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
113
|
});
|
|
114
|
-
|
|
115
|
-
// src/shared/constants.ts
|
|
116
|
-
var SESSION_TTL_SECONDS = 43200;
|
|
117
|
-
var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
|
|
118
|
-
var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
119
|
-
var DEFAULT_CLIENT_NAME = "MCP Assistant";
|
|
120
|
-
var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
|
|
121
|
-
var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
|
|
122
|
-
var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
|
|
123
|
-
var SOFTWARE_ID = "@mcp-ts";
|
|
124
|
-
var SOFTWARE_VERSION = "1.3.4";
|
|
125
|
-
var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
|
|
126
|
-
var MCP_CLIENT_VERSION = "2.0";
|
|
114
|
+
var OAUTH_STATE_SEPARATOR = ".";
|
|
127
115
|
var firstChar = customAlphabet(
|
|
128
116
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
|
129
117
|
1
|
|
@@ -142,13 +130,95 @@ function sanitizeServerLabel(name) {
|
|
|
142
130
|
function generateSessionId() {
|
|
143
131
|
return firstChar() + rest();
|
|
144
132
|
}
|
|
133
|
+
function formatOAuthState(nonce, sessionId) {
|
|
134
|
+
return `${nonce}${OAUTH_STATE_SEPARATOR}${sessionId}`;
|
|
135
|
+
}
|
|
136
|
+
function parseOAuthState(state) {
|
|
137
|
+
const separatorIndex = state.indexOf(OAUTH_STATE_SEPARATOR);
|
|
138
|
+
if (separatorIndex <= 0 || separatorIndex === state.length - 1) {
|
|
139
|
+
return void 0;
|
|
140
|
+
}
|
|
141
|
+
const nonce = state.slice(0, separatorIndex);
|
|
142
|
+
const sessionId = state.slice(separatorIndex + 1);
|
|
143
|
+
if (!nonce || !sessionId || sessionId.includes(OAUTH_STATE_SEPARATOR)) {
|
|
144
|
+
return void 0;
|
|
145
|
+
}
|
|
146
|
+
return { nonce, sessionId };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/shared/constants.ts
|
|
150
|
+
var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
|
|
151
|
+
var DORMANT_SESSION_EXPIRATION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
152
|
+
var DORMANT_SESSION_EXPIRATION_SECONDS = Math.floor(DORMANT_SESSION_EXPIRATION_MS / 1e3);
|
|
153
|
+
var DEFAULT_CLIENT_NAME = "MCP Assistant";
|
|
154
|
+
var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
|
|
155
|
+
var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.svg";
|
|
156
|
+
var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
|
|
157
|
+
var SOFTWARE_ID = "@mcp-ts";
|
|
158
|
+
var SOFTWARE_VERSION = "2.3.4";
|
|
159
|
+
var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
|
|
160
|
+
var MCP_CLIENT_VERSION = "2.0";
|
|
161
|
+
|
|
162
|
+
// src/server/storage/session-lifecycle.ts
|
|
163
|
+
function resolveSessionExpiresAt(status = "pending", referenceTime = Date.now()) {
|
|
164
|
+
return status === "active" ? null : referenceTime + STATE_EXPIRATION_MS;
|
|
165
|
+
}
|
|
166
|
+
function resolveSessionRedisTtlSeconds(session) {
|
|
167
|
+
return session.status === "active" ? DORMANT_SESSION_EXPIRATION_SECONDS : Math.floor(STATE_EXPIRATION_MS / 1e3);
|
|
168
|
+
}
|
|
169
|
+
function normalizeNewSession(session, now = Date.now()) {
|
|
170
|
+
const createdAt = session.createdAt || now;
|
|
171
|
+
const updatedAt = session.updatedAt ?? createdAt;
|
|
172
|
+
const status = session.status ?? "pending";
|
|
173
|
+
return {
|
|
174
|
+
...session,
|
|
175
|
+
status,
|
|
176
|
+
createdAt,
|
|
177
|
+
updatedAt,
|
|
178
|
+
expiresAt: resolveSessionExpiresAt(status, status === "active" ? updatedAt : createdAt)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function mergeSessionUpdate(current, data, now = Date.now()) {
|
|
182
|
+
const updatedAt = data.updatedAt ?? now;
|
|
183
|
+
const updated = {
|
|
184
|
+
...current,
|
|
185
|
+
...data,
|
|
186
|
+
updatedAt
|
|
187
|
+
};
|
|
188
|
+
const status = updated.status ?? "pending";
|
|
189
|
+
return {
|
|
190
|
+
...updated,
|
|
191
|
+
status,
|
|
192
|
+
expiresAt: resolveSessionExpiresAt(status, updatedAt)
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function normalizeStoredSession(session) {
|
|
196
|
+
const createdAt = session.createdAt || Date.now();
|
|
197
|
+
const updatedAt = session.updatedAt ?? createdAt;
|
|
198
|
+
const status = session.status ?? "pending";
|
|
199
|
+
const expiresAt = status === "active" ? null : session.expiresAt ?? resolveSessionExpiresAt(status, createdAt);
|
|
200
|
+
return {
|
|
201
|
+
...session,
|
|
202
|
+
status,
|
|
203
|
+
createdAt,
|
|
204
|
+
updatedAt,
|
|
205
|
+
expiresAt
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function isSessionExpired(session, now = Date.now()) {
|
|
209
|
+
const hydrated = normalizeStoredSession(session);
|
|
210
|
+
if (hydrated.status === "active") {
|
|
211
|
+
return hydrated.updatedAt !== void 0 && hydrated.updatedAt < now - DORMANT_SESSION_EXPIRATION_MS;
|
|
212
|
+
}
|
|
213
|
+
return typeof hydrated.expiresAt === "number" && hydrated.expiresAt < now;
|
|
214
|
+
}
|
|
145
215
|
|
|
146
216
|
// src/server/storage/redis-backend.ts
|
|
147
217
|
var RedisStorageBackend = class {
|
|
148
218
|
constructor(redis2) {
|
|
149
219
|
this.redis = redis2;
|
|
150
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
151
220
|
__publicField(this, "KEY_PREFIX", "mcp:session:");
|
|
221
|
+
__publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
|
|
152
222
|
__publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
|
|
153
223
|
__publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
|
|
154
224
|
}
|
|
@@ -167,6 +237,9 @@ var RedisStorageBackend = class {
|
|
|
167
237
|
getSessionKey(userId, sessionId) {
|
|
168
238
|
return `${this.KEY_PREFIX}${userId}:${sessionId}`;
|
|
169
239
|
}
|
|
240
|
+
getCredentialsKey(userId, sessionId) {
|
|
241
|
+
return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
|
|
242
|
+
}
|
|
170
243
|
/**
|
|
171
244
|
* Generates Redis key for tracking all sessions for a user
|
|
172
245
|
* @private
|
|
@@ -204,15 +277,16 @@ var RedisStorageBackend = class {
|
|
|
204
277
|
generateSessionId() {
|
|
205
278
|
return generateSessionId();
|
|
206
279
|
}
|
|
207
|
-
async create(session
|
|
280
|
+
async create(session) {
|
|
208
281
|
const { sessionId, userId } = session;
|
|
209
282
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
210
283
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
211
284
|
const userIdKey = this.getUserIdKey(userId);
|
|
212
|
-
const
|
|
285
|
+
const sessionWithLifecycle = normalizeNewSession(session);
|
|
286
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(sessionWithLifecycle);
|
|
213
287
|
const result = await this.redis.set(
|
|
214
288
|
sessionKey,
|
|
215
|
-
JSON.stringify(
|
|
289
|
+
JSON.stringify(sessionWithLifecycle),
|
|
216
290
|
"EX",
|
|
217
291
|
effectiveTtl,
|
|
218
292
|
"NX"
|
|
@@ -222,9 +296,8 @@ var RedisStorageBackend = class {
|
|
|
222
296
|
}
|
|
223
297
|
await this.redis.sadd(userIdKey, sessionId);
|
|
224
298
|
}
|
|
225
|
-
async update(userId, sessionId, data
|
|
299
|
+
async update(userId, sessionId, data) {
|
|
226
300
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
227
|
-
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
228
301
|
const script = `
|
|
229
302
|
local currentStr = redis.call("GET", KEYS[1])
|
|
230
303
|
if not currentStr then
|
|
@@ -232,26 +305,47 @@ var RedisStorageBackend = class {
|
|
|
232
305
|
end
|
|
233
306
|
|
|
234
307
|
local current = cjson.decode(currentStr)
|
|
235
|
-
local
|
|
236
|
-
|
|
237
|
-
for k,v in pairs(updates) do
|
|
238
|
-
current[k] = v
|
|
239
|
-
end
|
|
308
|
+
local updated = cjson.decode(ARGV[1])
|
|
240
309
|
|
|
241
|
-
redis.call("SET", KEYS[1], cjson.encode(
|
|
310
|
+
redis.call("SET", KEYS[1], cjson.encode(updated), "EX", ARGV[2])
|
|
242
311
|
return 1
|
|
243
312
|
`;
|
|
313
|
+
const current = await this.get(userId, sessionId);
|
|
314
|
+
if (!current) {
|
|
315
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
316
|
+
}
|
|
317
|
+
const updated = mergeSessionUpdate(current, data);
|
|
318
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
|
|
244
319
|
const result = await this.redis.eval(
|
|
245
320
|
script,
|
|
246
321
|
1,
|
|
247
322
|
sessionKey,
|
|
248
|
-
JSON.stringify(
|
|
323
|
+
JSON.stringify(updated),
|
|
249
324
|
effectiveTtl
|
|
250
325
|
);
|
|
251
326
|
if (result === 0) {
|
|
252
327
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
253
328
|
}
|
|
254
329
|
}
|
|
330
|
+
async patchCredentials(userId, sessionId, data) {
|
|
331
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
332
|
+
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
333
|
+
const sessionExists = await this.redis.exists(sessionKey);
|
|
334
|
+
if (!sessionExists) {
|
|
335
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
336
|
+
}
|
|
337
|
+
const session = await this.get(userId, sessionId);
|
|
338
|
+
const currentTtl = await this.redis.ttl(sessionKey);
|
|
339
|
+
const effectiveTtl = currentTtl > 0 && session ? currentTtl : resolveSessionRedisTtlSeconds(session ?? { status: "pending" });
|
|
340
|
+
const currentStr = await this.redis.get(credentialsKey);
|
|
341
|
+
const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
|
|
342
|
+
const credentials = { ...current, ...data, sessionId, userId };
|
|
343
|
+
if (effectiveTtl > 0) {
|
|
344
|
+
await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
|
|
345
|
+
} else {
|
|
346
|
+
await this.redis.set(credentialsKey, JSON.stringify(credentials));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
255
349
|
async get(userId, sessionId) {
|
|
256
350
|
try {
|
|
257
351
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
@@ -259,13 +353,28 @@ var RedisStorageBackend = class {
|
|
|
259
353
|
if (!sessionDataStr) {
|
|
260
354
|
return null;
|
|
261
355
|
}
|
|
262
|
-
const
|
|
263
|
-
return
|
|
356
|
+
const session = JSON.parse(sessionDataStr);
|
|
357
|
+
return normalizeStoredSession(session);
|
|
264
358
|
} catch (error) {
|
|
265
359
|
console.error("[RedisStorageBackend] Failed to get session:", error);
|
|
266
360
|
return null;
|
|
267
361
|
}
|
|
268
362
|
}
|
|
363
|
+
async getCredentials(userId, sessionId) {
|
|
364
|
+
const session = await this.get(userId, sessionId);
|
|
365
|
+
if (!session) return null;
|
|
366
|
+
const credentialsStr = await this.redis.get(this.getCredentialsKey(userId, sessionId));
|
|
367
|
+
return credentialsStr ? JSON.parse(credentialsStr) : { sessionId, userId };
|
|
368
|
+
}
|
|
369
|
+
async clearCredentials(userId, sessionId) {
|
|
370
|
+
await this.patchCredentials(userId, sessionId, {
|
|
371
|
+
clientInformation: null,
|
|
372
|
+
tokens: null,
|
|
373
|
+
codeVerifier: null,
|
|
374
|
+
clientId: null,
|
|
375
|
+
oauthState: null
|
|
376
|
+
});
|
|
377
|
+
}
|
|
269
378
|
async listIds(userId) {
|
|
270
379
|
const sessions2 = await this.list(userId);
|
|
271
380
|
return sessions2.map((session) => session.sessionId);
|
|
@@ -285,7 +394,7 @@ var RedisStorageBackend = class {
|
|
|
285
394
|
if (staleSessionIds.length > 0) {
|
|
286
395
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
287
396
|
}
|
|
288
|
-
return results.filter((session) => session !== null);
|
|
397
|
+
return results.filter((session) => session !== null).map((session) => normalizeStoredSession(session));
|
|
289
398
|
} catch (error) {
|
|
290
399
|
console.error(`[RedisStorageBackend] Failed to get session data for ${userId}:`, error);
|
|
291
400
|
return [];
|
|
@@ -294,9 +403,10 @@ var RedisStorageBackend = class {
|
|
|
294
403
|
async delete(userId, sessionId) {
|
|
295
404
|
try {
|
|
296
405
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
406
|
+
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
297
407
|
const userIdKey = this.getUserIdKey(userId);
|
|
298
408
|
await this.redis.srem(userIdKey, sessionId);
|
|
299
|
-
await this.redis.del(sessionKey);
|
|
409
|
+
await this.redis.del(sessionKey, credentialsKey);
|
|
300
410
|
} catch (error) {
|
|
301
411
|
console.error("[RedisStorageBackend] Failed to remove session:", error);
|
|
302
412
|
}
|
|
@@ -311,7 +421,7 @@ var RedisStorageBackend = class {
|
|
|
311
421
|
return null;
|
|
312
422
|
}
|
|
313
423
|
try {
|
|
314
|
-
return JSON.parse(data).sessionId;
|
|
424
|
+
return normalizeStoredSession(JSON.parse(data)).sessionId;
|
|
315
425
|
} catch (error) {
|
|
316
426
|
console.error("[RedisStorageBackend] Failed to parse session while listing all session IDs:", error);
|
|
317
427
|
return null;
|
|
@@ -327,8 +437,9 @@ var RedisStorageBackend = class {
|
|
|
327
437
|
async clearAll() {
|
|
328
438
|
try {
|
|
329
439
|
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
440
|
+
const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
|
|
330
441
|
const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
|
|
331
|
-
const allKeys = [...keys, ...userIdKeys];
|
|
442
|
+
const allKeys = [...keys, ...credentialKeys, ...userIdKeys];
|
|
332
443
|
if (allKeys.length > 0) {
|
|
333
444
|
await this.redis.del(...allKeys);
|
|
334
445
|
}
|
|
@@ -352,6 +463,7 @@ var RedisStorageBackend = class {
|
|
|
352
463
|
const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
|
|
353
464
|
if (staleSessionIds.length > 0) {
|
|
354
465
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
466
|
+
await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
|
|
355
467
|
}
|
|
356
468
|
const remainingCount = await this.redis.scard(userIdKey);
|
|
357
469
|
if (remainingCount === 0) {
|
|
@@ -376,6 +488,7 @@ var MemoryStorageBackend = class {
|
|
|
376
488
|
constructor() {
|
|
377
489
|
// Map<userId:sessionId, Session>
|
|
378
490
|
__publicField(this, "sessions", /* @__PURE__ */ new Map());
|
|
491
|
+
__publicField(this, "credentials", /* @__PURE__ */ new Map());
|
|
379
492
|
// Map<userId, Set<sessionId>>
|
|
380
493
|
__publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
|
|
381
494
|
}
|
|
@@ -388,36 +501,55 @@ var MemoryStorageBackend = class {
|
|
|
388
501
|
generateSessionId() {
|
|
389
502
|
return generateSessionId();
|
|
390
503
|
}
|
|
391
|
-
async create(session
|
|
504
|
+
async create(session) {
|
|
392
505
|
const { sessionId, userId } = session;
|
|
393
506
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
394
507
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
395
508
|
if (this.sessions.has(sessionKey)) {
|
|
396
509
|
throw new Error(`Session ${sessionId} already exists`);
|
|
397
510
|
}
|
|
398
|
-
this.sessions.set(sessionKey, session);
|
|
511
|
+
this.sessions.set(sessionKey, normalizeNewSession(session));
|
|
399
512
|
if (!this.userIdSessions.has(userId)) {
|
|
400
513
|
this.userIdSessions.set(userId, /* @__PURE__ */ new Set());
|
|
401
514
|
}
|
|
402
515
|
this.userIdSessions.get(userId).add(sessionId);
|
|
403
516
|
}
|
|
404
|
-
async update(userId, sessionId, data
|
|
517
|
+
async update(userId, sessionId, data) {
|
|
405
518
|
if (!userId || !sessionId) throw new Error("userId and sessionId required");
|
|
406
519
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
407
520
|
const current = this.sessions.get(sessionKey);
|
|
408
521
|
if (!current) {
|
|
409
522
|
throw new Error(`Session ${sessionId} not found`);
|
|
410
523
|
}
|
|
411
|
-
const updated =
|
|
412
|
-
...current,
|
|
413
|
-
...data
|
|
414
|
-
};
|
|
524
|
+
const updated = mergeSessionUpdate(current, data);
|
|
415
525
|
this.sessions.set(sessionKey, updated);
|
|
416
526
|
}
|
|
527
|
+
async patchCredentials(userId, sessionId, data) {
|
|
528
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
529
|
+
if (!this.sessions.has(sessionKey)) {
|
|
530
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
531
|
+
}
|
|
532
|
+
const current = this.credentials.get(sessionKey) ?? { sessionId, userId };
|
|
533
|
+
this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
534
|
+
}
|
|
417
535
|
async get(userId, sessionId) {
|
|
418
536
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
419
537
|
return this.sessions.get(sessionKey) || null;
|
|
420
538
|
}
|
|
539
|
+
async getCredentials(userId, sessionId) {
|
|
540
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
541
|
+
if (!this.sessions.has(sessionKey)) return null;
|
|
542
|
+
return this.credentials.get(sessionKey) ?? { sessionId, userId };
|
|
543
|
+
}
|
|
544
|
+
async clearCredentials(userId, sessionId) {
|
|
545
|
+
await this.patchCredentials(userId, sessionId, {
|
|
546
|
+
clientInformation: null,
|
|
547
|
+
tokens: null,
|
|
548
|
+
codeVerifier: null,
|
|
549
|
+
clientId: null,
|
|
550
|
+
oauthState: null
|
|
551
|
+
});
|
|
552
|
+
}
|
|
421
553
|
async listIds(userId) {
|
|
422
554
|
const set = this.userIdSessions.get(userId);
|
|
423
555
|
return set ? Array.from(set) : [];
|
|
@@ -437,6 +569,7 @@ var MemoryStorageBackend = class {
|
|
|
437
569
|
async delete(userId, sessionId) {
|
|
438
570
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
439
571
|
this.sessions.delete(sessionKey);
|
|
572
|
+
this.credentials.delete(sessionKey);
|
|
440
573
|
const set = this.userIdSessions.get(userId);
|
|
441
574
|
if (set) {
|
|
442
575
|
set.delete(sessionId);
|
|
@@ -450,9 +583,22 @@ var MemoryStorageBackend = class {
|
|
|
450
583
|
}
|
|
451
584
|
async clearAll() {
|
|
452
585
|
this.sessions.clear();
|
|
586
|
+
this.credentials.clear();
|
|
453
587
|
this.userIdSessions.clear();
|
|
454
588
|
}
|
|
455
589
|
async cleanupExpired() {
|
|
590
|
+
for (const [key, session] of this.sessions.entries()) {
|
|
591
|
+
if (!isSessionExpired(session)) continue;
|
|
592
|
+
this.sessions.delete(key);
|
|
593
|
+
this.credentials.delete(key);
|
|
594
|
+
const set = this.userIdSessions.get(session.userId);
|
|
595
|
+
if (set) {
|
|
596
|
+
set.delete(session.sessionId);
|
|
597
|
+
if (set.size === 0) {
|
|
598
|
+
this.userIdSessions.delete(session.userId);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
456
602
|
}
|
|
457
603
|
async disconnect() {
|
|
458
604
|
}
|
|
@@ -464,6 +610,7 @@ var FileStorageBackend = class {
|
|
|
464
610
|
constructor(options = {}) {
|
|
465
611
|
__publicField(this, "filePath");
|
|
466
612
|
__publicField(this, "memoryCache", null);
|
|
613
|
+
__publicField(this, "credentialsCache", null);
|
|
467
614
|
__publicField(this, "initialized", false);
|
|
468
615
|
this.filePath = options.path || "./sessions.json";
|
|
469
616
|
}
|
|
@@ -478,14 +625,22 @@ var FileStorageBackend = class {
|
|
|
478
625
|
const data = await promises.readFile(this.filePath, "utf-8");
|
|
479
626
|
const json = JSON.parse(data);
|
|
480
627
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
628
|
+
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
629
|
+
if (Array.isArray(json.sessions)) {
|
|
630
|
+
json.sessions.forEach((s) => {
|
|
631
|
+
const session = normalizeStoredSession(s);
|
|
632
|
+
this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (Array.isArray(json.credentials)) {
|
|
636
|
+
json.credentials.forEach((c) => {
|
|
637
|
+
this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
|
|
484
638
|
});
|
|
485
639
|
}
|
|
486
640
|
} catch (error) {
|
|
487
641
|
if (error.code === "ENOENT") {
|
|
488
642
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
643
|
+
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
489
644
|
await this.flush();
|
|
490
645
|
} else {
|
|
491
646
|
console.error("[FileStorage] Failed to load sessions:", error);
|
|
@@ -499,9 +654,11 @@ var FileStorageBackend = class {
|
|
|
499
654
|
if (!this.initialized) await this.init();
|
|
500
655
|
}
|
|
501
656
|
async flush() {
|
|
502
|
-
if (!this.memoryCache) return;
|
|
503
|
-
|
|
504
|
-
|
|
657
|
+
if (!this.memoryCache || !this.credentialsCache) return;
|
|
658
|
+
await promises.writeFile(this.filePath, JSON.stringify({
|
|
659
|
+
sessions: Array.from(this.memoryCache.values()),
|
|
660
|
+
credentials: Array.from(this.credentialsCache.values())
|
|
661
|
+
}, null, 2), "utf-8");
|
|
505
662
|
}
|
|
506
663
|
getSessionKey(userId, sessionId) {
|
|
507
664
|
return `${userId}:${sessionId}`;
|
|
@@ -509,7 +666,7 @@ var FileStorageBackend = class {
|
|
|
509
666
|
generateSessionId() {
|
|
510
667
|
return generateSessionId();
|
|
511
668
|
}
|
|
512
|
-
async create(session
|
|
669
|
+
async create(session) {
|
|
513
670
|
await this.ensureInitialized();
|
|
514
671
|
const { sessionId, userId } = session;
|
|
515
672
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
@@ -517,10 +674,10 @@ var FileStorageBackend = class {
|
|
|
517
674
|
if (this.memoryCache.has(sessionKey)) {
|
|
518
675
|
throw new Error(`Session ${sessionId} already exists`);
|
|
519
676
|
}
|
|
520
|
-
this.memoryCache.set(sessionKey, session);
|
|
677
|
+
this.memoryCache.set(sessionKey, normalizeNewSession(session));
|
|
521
678
|
await this.flush();
|
|
522
679
|
}
|
|
523
|
-
async update(userId, sessionId, data
|
|
680
|
+
async update(userId, sessionId, data) {
|
|
524
681
|
await this.ensureInitialized();
|
|
525
682
|
if (!userId || !sessionId) throw new Error("userId and sessionId required");
|
|
526
683
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
@@ -528,18 +685,40 @@ var FileStorageBackend = class {
|
|
|
528
685
|
if (!current) {
|
|
529
686
|
throw new Error(`Session ${sessionId} not found`);
|
|
530
687
|
}
|
|
531
|
-
const updated =
|
|
532
|
-
...current,
|
|
533
|
-
...data
|
|
534
|
-
};
|
|
688
|
+
const updated = mergeSessionUpdate(current, data);
|
|
535
689
|
this.memoryCache.set(sessionKey, updated);
|
|
536
690
|
await this.flush();
|
|
537
691
|
}
|
|
692
|
+
async patchCredentials(userId, sessionId, data) {
|
|
693
|
+
await this.ensureInitialized();
|
|
694
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
695
|
+
if (!this.memoryCache.has(sessionKey)) {
|
|
696
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
697
|
+
}
|
|
698
|
+
const current = this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
|
|
699
|
+
this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
700
|
+
await this.flush();
|
|
701
|
+
}
|
|
538
702
|
async get(userId, sessionId) {
|
|
539
703
|
await this.ensureInitialized();
|
|
540
704
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
541
705
|
return this.memoryCache.get(sessionKey) || null;
|
|
542
706
|
}
|
|
707
|
+
async getCredentials(userId, sessionId) {
|
|
708
|
+
await this.ensureInitialized();
|
|
709
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
710
|
+
if (!this.memoryCache.has(sessionKey)) return null;
|
|
711
|
+
return this.credentialsCache.get(sessionKey) ?? { sessionId, userId };
|
|
712
|
+
}
|
|
713
|
+
async clearCredentials(userId, sessionId) {
|
|
714
|
+
await this.patchCredentials(userId, sessionId, {
|
|
715
|
+
clientInformation: null,
|
|
716
|
+
tokens: null,
|
|
717
|
+
codeVerifier: null,
|
|
718
|
+
clientId: null,
|
|
719
|
+
oauthState: null
|
|
720
|
+
});
|
|
721
|
+
}
|
|
543
722
|
async list(userId) {
|
|
544
723
|
await this.ensureInitialized();
|
|
545
724
|
return Array.from(this.memoryCache.values()).filter((s) => s.userId === userId);
|
|
@@ -551,7 +730,9 @@ var FileStorageBackend = class {
|
|
|
551
730
|
async delete(userId, sessionId) {
|
|
552
731
|
await this.ensureInitialized();
|
|
553
732
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
554
|
-
|
|
733
|
+
const deleted = this.memoryCache.delete(sessionKey);
|
|
734
|
+
this.credentialsCache.delete(sessionKey);
|
|
735
|
+
if (deleted) {
|
|
555
736
|
await this.flush();
|
|
556
737
|
}
|
|
557
738
|
}
|
|
@@ -562,10 +743,21 @@ var FileStorageBackend = class {
|
|
|
562
743
|
async clearAll() {
|
|
563
744
|
await this.ensureInitialized();
|
|
564
745
|
this.memoryCache.clear();
|
|
746
|
+
this.credentialsCache.clear();
|
|
565
747
|
await this.flush();
|
|
566
748
|
}
|
|
567
749
|
async cleanupExpired() {
|
|
568
750
|
await this.ensureInitialized();
|
|
751
|
+
let changed = false;
|
|
752
|
+
for (const [key, session] of this.memoryCache.entries()) {
|
|
753
|
+
if (!isSessionExpired(session)) continue;
|
|
754
|
+
this.memoryCache.delete(key);
|
|
755
|
+
this.credentialsCache.delete(key);
|
|
756
|
+
changed = true;
|
|
757
|
+
}
|
|
758
|
+
if (changed) {
|
|
759
|
+
await this.flush();
|
|
760
|
+
}
|
|
569
761
|
}
|
|
570
762
|
async disconnect() {
|
|
571
763
|
}
|
|
@@ -574,10 +766,12 @@ var SqliteStorage = class {
|
|
|
574
766
|
constructor(options = {}) {
|
|
575
767
|
__publicField(this, "db", null);
|
|
576
768
|
__publicField(this, "table");
|
|
769
|
+
__publicField(this, "credentialsTable");
|
|
577
770
|
__publicField(this, "initialized", false);
|
|
578
771
|
__publicField(this, "dbPath");
|
|
579
772
|
this.dbPath = options.path || "./sessions.db";
|
|
580
773
|
this.table = options.table || "mcp_sessions";
|
|
774
|
+
this.credentialsTable = `${this.table}_credentials`;
|
|
581
775
|
}
|
|
582
776
|
async init() {
|
|
583
777
|
if (this.initialized) return;
|
|
@@ -588,6 +782,7 @@ var SqliteStorage = class {
|
|
|
588
782
|
fs2.mkdirSync(dir, { recursive: true });
|
|
589
783
|
}
|
|
590
784
|
this.db = new DatabaseConstructor(this.dbPath);
|
|
785
|
+
this.db.pragma("foreign_keys = ON");
|
|
591
786
|
this.db.exec(`
|
|
592
787
|
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
593
788
|
sessionId TEXT PRIMARY KEY,
|
|
@@ -596,6 +791,13 @@ var SqliteStorage = class {
|
|
|
596
791
|
expiresAt INTEGER
|
|
597
792
|
);
|
|
598
793
|
CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
|
|
794
|
+
CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
|
|
795
|
+
sessionId TEXT NOT NULL,
|
|
796
|
+
userId TEXT NOT NULL,
|
|
797
|
+
data TEXT NOT NULL,
|
|
798
|
+
PRIMARY KEY (userId, sessionId),
|
|
799
|
+
FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
|
|
800
|
+
);
|
|
599
801
|
`);
|
|
600
802
|
this.initialized = true;
|
|
601
803
|
console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
|
|
@@ -616,18 +818,18 @@ var SqliteStorage = class {
|
|
|
616
818
|
generateSessionId() {
|
|
617
819
|
return generateSessionId();
|
|
618
820
|
}
|
|
619
|
-
async create(session
|
|
821
|
+
async create(session) {
|
|
620
822
|
this.ensureInitialized();
|
|
621
823
|
const { sessionId, userId } = session;
|
|
622
824
|
if (!sessionId || !userId) {
|
|
623
825
|
throw new Error("userId and sessionId required");
|
|
624
826
|
}
|
|
625
|
-
const
|
|
827
|
+
const sessionWithLifecycle = normalizeNewSession(session);
|
|
626
828
|
try {
|
|
627
829
|
const stmt = this.db.prepare(
|
|
628
830
|
`INSERT INTO ${this.table} (sessionId, userId, data, expiresAt) VALUES (?, ?, ?, ?)`
|
|
629
831
|
);
|
|
630
|
-
stmt.run(sessionId, userId, JSON.stringify(
|
|
832
|
+
stmt.run(sessionId, userId, JSON.stringify(sessionWithLifecycle), sessionWithLifecycle.expiresAt ?? null);
|
|
631
833
|
} catch (error) {
|
|
632
834
|
if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
633
835
|
throw new Error(`Session ${sessionId} already exists`);
|
|
@@ -635,7 +837,7 @@ var SqliteStorage = class {
|
|
|
635
837
|
throw error;
|
|
636
838
|
}
|
|
637
839
|
}
|
|
638
|
-
async update(userId, sessionId, data
|
|
840
|
+
async update(userId, sessionId, data) {
|
|
639
841
|
this.ensureInitialized();
|
|
640
842
|
if (!sessionId || !userId) {
|
|
641
843
|
throw new Error("userId and sessionId required");
|
|
@@ -644,12 +846,25 @@ var SqliteStorage = class {
|
|
|
644
846
|
if (!currentSession) {
|
|
645
847
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
646
848
|
}
|
|
647
|
-
const updatedSession =
|
|
648
|
-
const expiresAt = ttl ? Date.now() + ttl * 1e3 : null;
|
|
849
|
+
const updatedSession = mergeSessionUpdate(currentSession, data);
|
|
649
850
|
const stmt = this.db.prepare(
|
|
650
851
|
`UPDATE ${this.table} SET data = ?, expiresAt = ? WHERE sessionId = ? AND userId = ?`
|
|
651
852
|
);
|
|
652
|
-
stmt.run(JSON.stringify(updatedSession), expiresAt, sessionId, userId);
|
|
853
|
+
stmt.run(JSON.stringify(updatedSession), updatedSession.expiresAt ?? null, sessionId, userId);
|
|
854
|
+
}
|
|
855
|
+
async patchCredentials(userId, sessionId, data) {
|
|
856
|
+
this.ensureInitialized();
|
|
857
|
+
if (!await this.get(userId, sessionId)) {
|
|
858
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
859
|
+
}
|
|
860
|
+
const current = await this.getCredentials(userId, sessionId) ?? { sessionId, userId };
|
|
861
|
+
const credentials = { ...current, ...data, sessionId, userId };
|
|
862
|
+
const stmt = this.db.prepare(
|
|
863
|
+
`INSERT INTO ${this.credentialsTable} (sessionId, userId, data)
|
|
864
|
+
VALUES (?, ?, ?)
|
|
865
|
+
ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
|
|
866
|
+
);
|
|
867
|
+
stmt.run(sessionId, userId, JSON.stringify(credentials));
|
|
653
868
|
}
|
|
654
869
|
async get(userId, sessionId) {
|
|
655
870
|
this.ensureInitialized();
|
|
@@ -658,7 +873,25 @@ var SqliteStorage = class {
|
|
|
658
873
|
);
|
|
659
874
|
const row = stmt.get(sessionId, userId);
|
|
660
875
|
if (!row) return null;
|
|
661
|
-
return JSON.parse(row.data);
|
|
876
|
+
return normalizeStoredSession(JSON.parse(row.data));
|
|
877
|
+
}
|
|
878
|
+
async getCredentials(userId, sessionId) {
|
|
879
|
+
this.ensureInitialized();
|
|
880
|
+
if (!await this.get(userId, sessionId)) return null;
|
|
881
|
+
const stmt = this.db.prepare(
|
|
882
|
+
`SELECT data FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
883
|
+
);
|
|
884
|
+
const row = stmt.get(sessionId, userId);
|
|
885
|
+
return row ? JSON.parse(row.data) : { sessionId, userId };
|
|
886
|
+
}
|
|
887
|
+
async clearCredentials(userId, sessionId) {
|
|
888
|
+
await this.patchCredentials(userId, sessionId, {
|
|
889
|
+
clientInformation: null,
|
|
890
|
+
tokens: null,
|
|
891
|
+
codeVerifier: null,
|
|
892
|
+
clientId: null,
|
|
893
|
+
oauthState: null
|
|
894
|
+
});
|
|
662
895
|
}
|
|
663
896
|
async list(userId) {
|
|
664
897
|
this.ensureInitialized();
|
|
@@ -666,7 +899,7 @@ var SqliteStorage = class {
|
|
|
666
899
|
`SELECT data FROM ${this.table} WHERE userId = ?`
|
|
667
900
|
);
|
|
668
901
|
const rows = stmt.all(userId);
|
|
669
|
-
return rows.map((row) => JSON.parse(row.data));
|
|
902
|
+
return rows.map((row) => normalizeStoredSession(JSON.parse(row.data)));
|
|
670
903
|
}
|
|
671
904
|
async listIds(userId) {
|
|
672
905
|
this.ensureInitialized();
|
|
@@ -681,6 +914,9 @@ var SqliteStorage = class {
|
|
|
681
914
|
const stmt = this.db.prepare(
|
|
682
915
|
`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
683
916
|
);
|
|
917
|
+
this.db.prepare(
|
|
918
|
+
`DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
919
|
+
).run(sessionId, userId);
|
|
684
920
|
stmt.run(sessionId, userId);
|
|
685
921
|
}
|
|
686
922
|
async listAllIds() {
|
|
@@ -691,16 +927,28 @@ var SqliteStorage = class {
|
|
|
691
927
|
}
|
|
692
928
|
async clearAll() {
|
|
693
929
|
this.ensureInitialized();
|
|
930
|
+
this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
|
|
694
931
|
const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
|
|
695
932
|
stmt.run();
|
|
696
933
|
}
|
|
697
934
|
async cleanupExpired() {
|
|
698
935
|
this.ensureInitialized();
|
|
699
|
-
const
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
936
|
+
const rows = this.db.prepare(`SELECT sessionId, userId, data FROM ${this.table}`).all();
|
|
937
|
+
const deleteStmt = this.db.prepare(`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`);
|
|
938
|
+
for (const row of rows) {
|
|
939
|
+
const session = normalizeStoredSession(JSON.parse(row.data));
|
|
940
|
+
if (isSessionExpired(session)) {
|
|
941
|
+
deleteStmt.run(row.sessionId, row.userId);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
this.db.prepare(
|
|
945
|
+
`DELETE FROM ${this.credentialsTable}
|
|
946
|
+
WHERE NOT EXISTS (
|
|
947
|
+
SELECT 1 FROM ${this.table}
|
|
948
|
+
WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
|
|
949
|
+
AND ${this.table}.userId = ${this.credentialsTable}.userId
|
|
950
|
+
)`
|
|
951
|
+
).run();
|
|
704
952
|
}
|
|
705
953
|
async disconnect() {
|
|
706
954
|
if (this.db) {
|
|
@@ -779,19 +1027,21 @@ function decryptObject(data) {
|
|
|
779
1027
|
var SupabaseStorageBackend = class {
|
|
780
1028
|
constructor(supabase) {
|
|
781
1029
|
this.supabase = supabase;
|
|
782
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
783
1030
|
}
|
|
784
1031
|
async init() {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1032
|
+
await this.assertTable("mcp_sessions", "session_id");
|
|
1033
|
+
await this.assertTable("mcp_credentials", "session_id");
|
|
1034
|
+
console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
|
|
1035
|
+
}
|
|
1036
|
+
async assertTable(table, column) {
|
|
1037
|
+
const { error } = await this.supabase.from(table).select(column).limit(0);
|
|
1038
|
+
if (!error) return;
|
|
1039
|
+
if (error.code === "42P01") {
|
|
1040
|
+
throw new Error(
|
|
1041
|
+
`[SupabaseStorage] Table "${table}" not found in your database. Please run "npx mcp-ts supabase-init" to set up the required storage schema.`
|
|
1042
|
+
);
|
|
793
1043
|
}
|
|
794
|
-
|
|
1044
|
+
throw new Error(`[SupabaseStorage] Initialization check failed for "${table}": ${error.message}`);
|
|
795
1045
|
}
|
|
796
1046
|
generateSessionId() {
|
|
797
1047
|
return generateSessionId();
|
|
@@ -805,37 +1055,49 @@ var SupabaseStorageBackend = class {
|
|
|
805
1055
|
transportType: row.transport_type,
|
|
806
1056
|
callbackUrl: row.callback_url,
|
|
807
1057
|
createdAt: new Date(row.created_at).getTime(),
|
|
1058
|
+
updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
|
|
1059
|
+
expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
|
|
808
1060
|
userId: row.user_id,
|
|
809
1061
|
headers: decryptObject(row.headers),
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1062
|
+
authUrl: row.auth_url,
|
|
1063
|
+
status: row.status ?? "pending"
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
mapRowToCredentials(row, userId, sessionId) {
|
|
1067
|
+
return {
|
|
1068
|
+
sessionId,
|
|
1069
|
+
userId,
|
|
1070
|
+
clientInformation: decryptObject(row?.client_information),
|
|
1071
|
+
tokens: decryptObject(row?.tokens),
|
|
1072
|
+
codeVerifier: decryptObject(row?.code_verifier),
|
|
1073
|
+
clientId: row?.client_id,
|
|
1074
|
+
oauthState: row?.oauth_state
|
|
815
1075
|
};
|
|
816
1076
|
}
|
|
817
|
-
|
|
1077
|
+
hasCredentialData(data) {
|
|
1078
|
+
return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
|
|
1079
|
+
}
|
|
1080
|
+
async create(session) {
|
|
818
1081
|
const { sessionId, userId } = session;
|
|
819
1082
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
820
|
-
const
|
|
821
|
-
const
|
|
1083
|
+
const status = session.status ?? "pending";
|
|
1084
|
+
const createdAt = new Date(session.createdAt || Date.now()).toISOString();
|
|
1085
|
+
const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
|
|
1086
|
+
const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
|
|
822
1087
|
const { error } = await this.supabase.from("mcp_sessions").insert({
|
|
823
1088
|
session_id: sessionId,
|
|
824
1089
|
user_id: userId,
|
|
825
|
-
// Maps user_id to userId to support RLS using auth.uid()
|
|
826
1090
|
server_id: session.serverId,
|
|
827
1091
|
server_name: session.serverName,
|
|
828
1092
|
server_url: session.serverUrl,
|
|
829
1093
|
transport_type: session.transportType,
|
|
830
1094
|
callback_url: session.callbackUrl,
|
|
831
|
-
created_at:
|
|
1095
|
+
created_at: createdAt,
|
|
1096
|
+
updated_at: updatedAt,
|
|
832
1097
|
headers: encryptObject(session.headers),
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
code_verifier: session.codeVerifier,
|
|
837
|
-
client_id: session.clientId,
|
|
838
|
-
expires_at: expiresAt
|
|
1098
|
+
auth_url: session.authUrl ?? null,
|
|
1099
|
+
status,
|
|
1100
|
+
expires_at: expiresAt === null ? null : new Date(expiresAt).toISOString()
|
|
839
1101
|
});
|
|
840
1102
|
if (error) {
|
|
841
1103
|
if (error.code === "23505") {
|
|
@@ -844,11 +1106,8 @@ var SupabaseStorageBackend = class {
|
|
|
844
1106
|
throw new Error(`Failed to create session in Supabase: ${error.message}`);
|
|
845
1107
|
}
|
|
846
1108
|
}
|
|
847
|
-
async update(userId, sessionId, data
|
|
848
|
-
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
849
|
-
const expiresAt = new Date(Date.now() + effectiveTtl * 1e3).toISOString();
|
|
1109
|
+
async update(userId, sessionId, data) {
|
|
850
1110
|
const updateData = {
|
|
851
|
-
expires_at: expiresAt,
|
|
852
1111
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
853
1112
|
};
|
|
854
1113
|
if ("serverId" in data) updateData.server_id = data.serverId;
|
|
@@ -856,20 +1115,50 @@ var SupabaseStorageBackend = class {
|
|
|
856
1115
|
if ("serverUrl" in data) updateData.server_url = data.serverUrl;
|
|
857
1116
|
if ("transportType" in data) updateData.transport_type = data.transportType;
|
|
858
1117
|
if ("callbackUrl" in data) updateData.callback_url = data.callbackUrl;
|
|
859
|
-
if ("
|
|
1118
|
+
if ("status" in data) {
|
|
1119
|
+
const status = data.status ?? "pending";
|
|
1120
|
+
const expiresAt = resolveSessionExpiresAt(status);
|
|
1121
|
+
updateData.status = status;
|
|
1122
|
+
updateData.expires_at = expiresAt === null ? null : new Date(expiresAt).toISOString();
|
|
1123
|
+
}
|
|
860
1124
|
if ("headers" in data) updateData.headers = encryptObject(data.headers);
|
|
861
|
-
if ("
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
if (
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1125
|
+
if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
|
|
1126
|
+
const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
|
|
1127
|
+
let updatedRows = null;
|
|
1128
|
+
if (shouldUpdateSession) {
|
|
1129
|
+
const result = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId).select("id");
|
|
1130
|
+
if (result.error) {
|
|
1131
|
+
throw new Error(`Failed to update session: ${result.error.message}`);
|
|
1132
|
+
}
|
|
1133
|
+
updatedRows = result.data;
|
|
1134
|
+
} else {
|
|
1135
|
+
const result = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
|
|
1136
|
+
if (result.error) {
|
|
1137
|
+
throw new Error(`Failed to update session: ${result.error.message}`);
|
|
1138
|
+
}
|
|
1139
|
+
updatedRows = result.data;
|
|
868
1140
|
}
|
|
869
1141
|
if (!updatedRows || updatedRows.length === 0) {
|
|
870
1142
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
871
1143
|
}
|
|
872
1144
|
}
|
|
1145
|
+
async patchCredentials(userId, sessionId, data) {
|
|
1146
|
+
if (!this.hasCredentialData(data)) return;
|
|
1147
|
+
const row = {
|
|
1148
|
+
user_id: userId,
|
|
1149
|
+
session_id: sessionId,
|
|
1150
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1151
|
+
};
|
|
1152
|
+
if ("clientInformation" in data) row.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
|
|
1153
|
+
if ("tokens" in data) row.tokens = data.tokens == null ? null : encryptObject(data.tokens);
|
|
1154
|
+
if ("codeVerifier" in data) row.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
|
|
1155
|
+
if ("clientId" in data) row.client_id = data.clientId ?? null;
|
|
1156
|
+
if ("oauthState" in data) row.oauth_state = data.oauthState ?? null;
|
|
1157
|
+
const { error } = await this.supabase.from("mcp_credentials").upsert(row, { onConflict: "user_id,session_id" });
|
|
1158
|
+
if (error) {
|
|
1159
|
+
throw new Error(`Failed to update credentials: ${error.message}`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
873
1162
|
async get(userId, sessionId) {
|
|
874
1163
|
const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
875
1164
|
if (error) {
|
|
@@ -879,6 +1168,21 @@ var SupabaseStorageBackend = class {
|
|
|
879
1168
|
if (!data) return null;
|
|
880
1169
|
return this.mapRowToSessionData(data);
|
|
881
1170
|
}
|
|
1171
|
+
async getCredentials(userId, sessionId) {
|
|
1172
|
+
const { data, error } = await this.supabase.from("mcp_credentials").select("*").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
1173
|
+
if (error) {
|
|
1174
|
+
console.error("[SupabaseStorage] Failed to get credentials:", error);
|
|
1175
|
+
return null;
|
|
1176
|
+
}
|
|
1177
|
+
if (data) {
|
|
1178
|
+
return this.mapRowToCredentials(data, userId, sessionId);
|
|
1179
|
+
}
|
|
1180
|
+
const { data: sessionRows, error: sessionError } = await this.supabase.from("mcp_sessions").select("id").eq("user_id", userId).eq("session_id", sessionId);
|
|
1181
|
+
if (sessionError || !sessionRows || sessionRows.length === 0) {
|
|
1182
|
+
return null;
|
|
1183
|
+
}
|
|
1184
|
+
return { sessionId, userId };
|
|
1185
|
+
}
|
|
882
1186
|
async list(userId) {
|
|
883
1187
|
const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
|
|
884
1188
|
if (error) {
|
|
@@ -887,6 +1191,12 @@ var SupabaseStorageBackend = class {
|
|
|
887
1191
|
}
|
|
888
1192
|
return data.map((row) => this.mapRowToSessionData(row));
|
|
889
1193
|
}
|
|
1194
|
+
async clearCredentials(userId, sessionId) {
|
|
1195
|
+
const { error } = await this.supabase.from("mcp_credentials").delete().eq("user_id", userId).eq("session_id", sessionId);
|
|
1196
|
+
if (error) {
|
|
1197
|
+
throw new Error(`Failed to clear credentials: ${error.message}`);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
890
1200
|
async delete(userId, sessionId) {
|
|
891
1201
|
const { error } = await this.supabase.from("mcp_sessions").delete().eq("user_id", userId).eq("session_id", sessionId);
|
|
892
1202
|
if (error) {
|
|
@@ -910,15 +1220,24 @@ var SupabaseStorageBackend = class {
|
|
|
910
1220
|
return data.map((row) => row.session_id);
|
|
911
1221
|
}
|
|
912
1222
|
async clearAll() {
|
|
1223
|
+
const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
|
|
1224
|
+
if (credentialsError) {
|
|
1225
|
+
console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
|
|
1226
|
+
}
|
|
913
1227
|
const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
|
|
914
1228
|
if (error) {
|
|
915
1229
|
console.error("[SupabaseStorage] Failed to clear sessions:", error);
|
|
916
1230
|
}
|
|
917
1231
|
}
|
|
918
1232
|
async cleanupExpired() {
|
|
919
|
-
const { error } = await this.supabase.from("mcp_sessions").delete().lt("expires_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
920
|
-
if (
|
|
921
|
-
console.error("[SupabaseStorage] Failed to cleanup expired sessions:",
|
|
1233
|
+
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());
|
|
1234
|
+
if (transientError) {
|
|
1235
|
+
console.error("[SupabaseStorage] Failed to cleanup expired inactive sessions:", transientError);
|
|
1236
|
+
}
|
|
1237
|
+
const dormantCutoff = new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString();
|
|
1238
|
+
const { error: dormantError } = await this.supabase.from("mcp_sessions").delete().eq("status", "active").lt("updated_at", dormantCutoff);
|
|
1239
|
+
if (dormantError) {
|
|
1240
|
+
console.error("[SupabaseStorage] Failed to cleanup dormant active sessions:", dormantError);
|
|
922
1241
|
}
|
|
923
1242
|
}
|
|
924
1243
|
async disconnect() {
|
|
@@ -929,23 +1248,29 @@ var SupabaseStorageBackend = class {
|
|
|
929
1248
|
var NeonStorageBackend = class {
|
|
930
1249
|
constructor(sql, options = {}) {
|
|
931
1250
|
this.sql = sql;
|
|
932
|
-
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
933
1251
|
__publicField(this, "tableName");
|
|
1252
|
+
__publicField(this, "credentialsTableName");
|
|
934
1253
|
const schema = options.schema || "public";
|
|
935
1254
|
const table = options.table || "mcp_sessions";
|
|
1255
|
+
const credentialsTable = options.credentialsTable || "mcp_credentials";
|
|
936
1256
|
this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
|
|
1257
|
+
this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
|
|
937
1258
|
}
|
|
938
1259
|
async init() {
|
|
1260
|
+
await this.assertTable(this.tableName, "mcp_sessions");
|
|
1261
|
+
await this.assertTable(this.credentialsTableName, "mcp_credentials");
|
|
1262
|
+
console.log("[mcp-ts][Storage] Neon: storage tables verified.");
|
|
1263
|
+
}
|
|
1264
|
+
async assertTable(qualifiedName, displayName) {
|
|
939
1265
|
const [{ exists } = { exists: null }] = await this.sql.query(
|
|
940
1266
|
"SELECT to_regclass($1) AS exists",
|
|
941
|
-
[
|
|
1267
|
+
[qualifiedName.replace(/"/g, "")]
|
|
942
1268
|
);
|
|
943
1269
|
if (!exists) {
|
|
944
1270
|
throw new Error(
|
|
945
|
-
|
|
1271
|
+
`[NeonStorage] Table "${displayName}" not found in your database. Please create it using the Neon storage guide in docs/storage-backends/neon.md.`
|
|
946
1272
|
);
|
|
947
1273
|
}
|
|
948
|
-
console.log('[mcp-ts][Storage] Neon: "mcp_sessions" table verified.');
|
|
949
1274
|
}
|
|
950
1275
|
generateSessionId() {
|
|
951
1276
|
return generateSessionId();
|
|
@@ -965,20 +1290,35 @@ var NeonStorageBackend = class {
|
|
|
965
1290
|
transportType: row.transport_type,
|
|
966
1291
|
callbackUrl: row.callback_url,
|
|
967
1292
|
createdAt: new Date(row.created_at).getTime(),
|
|
1293
|
+
updatedAt: new Date(row.updated_at ?? row.created_at).getTime(),
|
|
1294
|
+
expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : null,
|
|
968
1295
|
userId: row.user_id,
|
|
969
1296
|
headers: decryptObject(row.headers),
|
|
970
|
-
|
|
971
|
-
|
|
1297
|
+
authUrl: row.auth_url ?? void 0,
|
|
1298
|
+
status: row.status ?? "pending"
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
mapRowToCredentials(row, userId, sessionId) {
|
|
1302
|
+
return {
|
|
1303
|
+
sessionId,
|
|
1304
|
+
userId,
|
|
1305
|
+
clientInformation: decryptObject(row.client_information),
|
|
972
1306
|
tokens: decryptObject(row.tokens),
|
|
973
|
-
codeVerifier: row.code_verifier
|
|
974
|
-
clientId: row.client_id ?? void 0
|
|
1307
|
+
codeVerifier: decryptObject(row.code_verifier),
|
|
1308
|
+
clientId: row.client_id ?? void 0,
|
|
1309
|
+
oauthState: row.oauth_state
|
|
975
1310
|
};
|
|
976
1311
|
}
|
|
977
|
-
|
|
1312
|
+
hasCredentialData(data) {
|
|
1313
|
+
return "clientInformation" in data || "tokens" in data || "codeVerifier" in data || "clientId" in data || "oauthState" in data;
|
|
1314
|
+
}
|
|
1315
|
+
async create(session) {
|
|
978
1316
|
const { sessionId, userId } = session;
|
|
979
1317
|
if (!sessionId || !userId) throw new Error("userId and sessionId required");
|
|
980
|
-
const
|
|
981
|
-
const
|
|
1318
|
+
const status = session.status ?? "pending";
|
|
1319
|
+
const createdAt = new Date(session.createdAt || Date.now()).toISOString();
|
|
1320
|
+
const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
|
|
1321
|
+
const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
|
|
982
1322
|
try {
|
|
983
1323
|
await this.sql.query(
|
|
984
1324
|
`INSERT INTO ${this.tableName} (
|
|
@@ -990,16 +1330,14 @@ var NeonStorageBackend = class {
|
|
|
990
1330
|
transport_type,
|
|
991
1331
|
callback_url,
|
|
992
1332
|
created_at,
|
|
1333
|
+
updated_at,
|
|
993
1334
|
headers,
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
tokens,
|
|
997
|
-
code_verifier,
|
|
998
|
-
client_id,
|
|
1335
|
+
auth_url,
|
|
1336
|
+
status,
|
|
999
1337
|
expires_at
|
|
1000
1338
|
) VALUES (
|
|
1001
1339
|
$1, $2, $3, $4, $5, $6, $7, $8,
|
|
1002
|
-
$9, $10, $11, $12, $13
|
|
1340
|
+
$9, $10, $11, $12, $13
|
|
1003
1341
|
)`,
|
|
1004
1342
|
[
|
|
1005
1343
|
sessionId,
|
|
@@ -1009,14 +1347,12 @@ var NeonStorageBackend = class {
|
|
|
1009
1347
|
session.serverUrl,
|
|
1010
1348
|
session.transportType,
|
|
1011
1349
|
session.callbackUrl,
|
|
1012
|
-
|
|
1350
|
+
createdAt,
|
|
1351
|
+
updatedAt,
|
|
1013
1352
|
encryptObject(session.headers),
|
|
1014
|
-
session.
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
session.codeVerifier,
|
|
1018
|
-
session.clientId,
|
|
1019
|
-
expiresAt
|
|
1353
|
+
session.authUrl ?? null,
|
|
1354
|
+
status,
|
|
1355
|
+
expiresAt === null ? null : new Date(expiresAt).toISOString()
|
|
1020
1356
|
]
|
|
1021
1357
|
);
|
|
1022
1358
|
} catch (error) {
|
|
@@ -1026,52 +1362,86 @@ var NeonStorageBackend = class {
|
|
|
1026
1362
|
throw new Error(`Failed to create session in Neon: ${error.message}`);
|
|
1027
1363
|
}
|
|
1028
1364
|
}
|
|
1029
|
-
async update(userId, sessionId, data
|
|
1365
|
+
async update(userId, sessionId, data) {
|
|
1030
1366
|
const currentSession = await this.get(userId, sessionId);
|
|
1031
1367
|
if (!currentSession) {
|
|
1032
1368
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1033
1369
|
}
|
|
1034
1370
|
const updatedSession = { ...currentSession, ...data };
|
|
1035
|
-
const
|
|
1036
|
-
const expiresAt =
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1371
|
+
const status = updatedSession.status ?? "pending";
|
|
1372
|
+
const expiresAt = resolveSessionExpiresAt(status);
|
|
1373
|
+
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;
|
|
1374
|
+
if (shouldUpdateSession) {
|
|
1375
|
+
const updatedRows = await this.sql.query(
|
|
1376
|
+
`UPDATE ${this.tableName}
|
|
1377
|
+
SET
|
|
1378
|
+
server_id = $1,
|
|
1379
|
+
server_name = $2,
|
|
1380
|
+
server_url = $3,
|
|
1381
|
+
transport_type = $4,
|
|
1382
|
+
callback_url = $5,
|
|
1383
|
+
status = $6,
|
|
1384
|
+
headers = $7,
|
|
1385
|
+
auth_url = $8,
|
|
1386
|
+
expires_at = $9,
|
|
1387
|
+
updated_at = now()
|
|
1388
|
+
WHERE user_id = $10 AND session_id = $11
|
|
1389
|
+
RETURNING id`,
|
|
1390
|
+
[
|
|
1391
|
+
updatedSession.serverId,
|
|
1392
|
+
updatedSession.serverName,
|
|
1393
|
+
updatedSession.serverUrl,
|
|
1394
|
+
updatedSession.transportType,
|
|
1395
|
+
updatedSession.callbackUrl,
|
|
1396
|
+
status,
|
|
1397
|
+
encryptObject(updatedSession.headers),
|
|
1398
|
+
updatedSession.authUrl ?? null,
|
|
1399
|
+
expiresAt === null ? null : new Date(expiresAt).toISOString(),
|
|
1400
|
+
userId,
|
|
1401
|
+
sessionId
|
|
1402
|
+
]
|
|
1403
|
+
);
|
|
1404
|
+
if (updatedRows.length === 0) {
|
|
1405
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
async patchCredentials(userId, sessionId, data) {
|
|
1410
|
+
if (!this.hasCredentialData(data)) return;
|
|
1411
|
+
await this.sql.query(
|
|
1412
|
+
`INSERT INTO ${this.credentialsTableName} (
|
|
1413
|
+
user_id,
|
|
1414
|
+
session_id,
|
|
1415
|
+
client_information,
|
|
1416
|
+
tokens,
|
|
1417
|
+
code_verifier,
|
|
1418
|
+
client_id,
|
|
1419
|
+
oauth_state,
|
|
1420
|
+
updated_at
|
|
1421
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, now())
|
|
1422
|
+
ON CONFLICT (user_id, session_id)
|
|
1423
|
+
DO UPDATE SET
|
|
1424
|
+
client_information = CASE WHEN $8 THEN EXCLUDED.client_information ELSE ${this.credentialsTableName}.client_information END,
|
|
1425
|
+
tokens = CASE WHEN $9 THEN EXCLUDED.tokens ELSE ${this.credentialsTableName}.tokens END,
|
|
1426
|
+
code_verifier = CASE WHEN $10 THEN EXCLUDED.code_verifier ELSE ${this.credentialsTableName}.code_verifier END,
|
|
1427
|
+
client_id = CASE WHEN $11 THEN EXCLUDED.client_id ELSE ${this.credentialsTableName}.client_id END,
|
|
1428
|
+
oauth_state = CASE WHEN $12 THEN EXCLUDED.oauth_state ELSE ${this.credentialsTableName}.oauth_state END,
|
|
1429
|
+
updated_at = now()`,
|
|
1055
1430
|
[
|
|
1056
|
-
updatedSession.serverId,
|
|
1057
|
-
updatedSession.serverName,
|
|
1058
|
-
updatedSession.serverUrl,
|
|
1059
|
-
updatedSession.transportType,
|
|
1060
|
-
updatedSession.callbackUrl,
|
|
1061
|
-
updatedSession.active ?? false,
|
|
1062
|
-
encryptObject(updatedSession.headers),
|
|
1063
|
-
updatedSession.clientInformation,
|
|
1064
|
-
encryptObject(updatedSession.tokens),
|
|
1065
|
-
updatedSession.codeVerifier,
|
|
1066
|
-
updatedSession.clientId,
|
|
1067
|
-
expiresAt,
|
|
1068
1431
|
userId,
|
|
1069
|
-
sessionId
|
|
1432
|
+
sessionId,
|
|
1433
|
+
"clientInformation" in data ? data.clientInformation == null ? null : encryptObject(data.clientInformation) : null,
|
|
1434
|
+
"tokens" in data ? data.tokens == null ? null : encryptObject(data.tokens) : null,
|
|
1435
|
+
"codeVerifier" in data ? data.codeVerifier == null ? null : encryptObject(data.codeVerifier) : null,
|
|
1436
|
+
"clientId" in data ? data.clientId ?? null : null,
|
|
1437
|
+
"oauthState" in data ? data.oauthState ?? null : null,
|
|
1438
|
+
"clientInformation" in data,
|
|
1439
|
+
"tokens" in data,
|
|
1440
|
+
"codeVerifier" in data,
|
|
1441
|
+
"clientId" in data,
|
|
1442
|
+
"oauthState" in data
|
|
1070
1443
|
]
|
|
1071
1444
|
);
|
|
1072
|
-
if (updatedRows.length === 0) {
|
|
1073
|
-
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1074
|
-
}
|
|
1075
1445
|
}
|
|
1076
1446
|
async get(userId, sessionId) {
|
|
1077
1447
|
try {
|
|
@@ -1079,12 +1449,32 @@ var NeonStorageBackend = class {
|
|
|
1079
1449
|
`SELECT * FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1080
1450
|
[userId, sessionId]
|
|
1081
1451
|
);
|
|
1082
|
-
|
|
1452
|
+
if (!rows[0]) return null;
|
|
1453
|
+
return this.mapRowToSessionData(rows[0]);
|
|
1083
1454
|
} catch (error) {
|
|
1084
1455
|
console.error("[NeonStorage] Failed to get session:", error);
|
|
1085
1456
|
return null;
|
|
1086
1457
|
}
|
|
1087
1458
|
}
|
|
1459
|
+
async getCredentials(userId, sessionId) {
|
|
1460
|
+
try {
|
|
1461
|
+
const credentialRows = await this.sql.query(
|
|
1462
|
+
`SELECT * FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1463
|
+
[userId, sessionId]
|
|
1464
|
+
);
|
|
1465
|
+
if (credentialRows[0]) {
|
|
1466
|
+
return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
|
|
1467
|
+
}
|
|
1468
|
+
const sessionRows = await this.sql.query(
|
|
1469
|
+
`SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1470
|
+
[userId, sessionId]
|
|
1471
|
+
);
|
|
1472
|
+
return sessionRows[0] ? { sessionId, userId } : null;
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
console.error("[NeonStorage] Failed to get credentials:", error);
|
|
1475
|
+
return null;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1088
1478
|
async list(userId) {
|
|
1089
1479
|
try {
|
|
1090
1480
|
const rows = await this.sql.query(
|
|
@@ -1097,6 +1487,16 @@ var NeonStorageBackend = class {
|
|
|
1097
1487
|
return [];
|
|
1098
1488
|
}
|
|
1099
1489
|
}
|
|
1490
|
+
async clearCredentials(userId, sessionId) {
|
|
1491
|
+
try {
|
|
1492
|
+
await this.sql.query(
|
|
1493
|
+
`DELETE FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1494
|
+
[userId, sessionId]
|
|
1495
|
+
);
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
console.error("[NeonStorage] Failed to clear credentials:", error);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1100
1500
|
async delete(userId, sessionId) {
|
|
1101
1501
|
try {
|
|
1102
1502
|
await this.sql.query(
|
|
@@ -1132,6 +1532,7 @@ var NeonStorageBackend = class {
|
|
|
1132
1532
|
}
|
|
1133
1533
|
async clearAll() {
|
|
1134
1534
|
try {
|
|
1535
|
+
await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
|
|
1135
1536
|
await this.sql.query(`DELETE FROM ${this.tableName}`);
|
|
1136
1537
|
} catch (error) {
|
|
1137
1538
|
console.error("[NeonStorage] Failed to clear sessions:", error);
|
|
@@ -1140,9 +1541,17 @@ var NeonStorageBackend = class {
|
|
|
1140
1541
|
async cleanupExpired() {
|
|
1141
1542
|
try {
|
|
1142
1543
|
await this.sql.query(
|
|
1143
|
-
`DELETE FROM ${this.tableName}
|
|
1544
|
+
`DELETE FROM ${this.tableName}
|
|
1545
|
+
WHERE expires_at IS NOT NULL
|
|
1546
|
+
AND expires_at < $1
|
|
1547
|
+
AND status <> 'active'`,
|
|
1144
1548
|
[(/* @__PURE__ */ new Date()).toISOString()]
|
|
1145
1549
|
);
|
|
1550
|
+
await this.sql.query(
|
|
1551
|
+
`DELETE FROM ${this.tableName}
|
|
1552
|
+
WHERE status = 'active' AND updated_at < $1`,
|
|
1553
|
+
[new Date(Date.now() - DORMANT_SESSION_EXPIRATION_MS).toISOString()]
|
|
1554
|
+
);
|
|
1146
1555
|
} catch (error) {
|
|
1147
1556
|
console.error("[NeonStorage] Failed to cleanup expired sessions:", error);
|
|
1148
1557
|
}
|
|
@@ -1189,26 +1598,24 @@ function emitSessionMutation(event) {
|
|
|
1189
1598
|
function createSessionMutationEvent(prop, args) {
|
|
1190
1599
|
const timestamp = Date.now();
|
|
1191
1600
|
if (prop === "create") {
|
|
1192
|
-
const [session
|
|
1601
|
+
const [session] = args;
|
|
1193
1602
|
if (!session?.userId || !session?.sessionId) return null;
|
|
1194
1603
|
return {
|
|
1195
1604
|
type: "create",
|
|
1196
1605
|
userId: session.userId,
|
|
1197
1606
|
sessionId: session.sessionId,
|
|
1198
1607
|
session,
|
|
1199
|
-
ttl,
|
|
1200
1608
|
timestamp
|
|
1201
1609
|
};
|
|
1202
1610
|
}
|
|
1203
1611
|
if (prop === "update") {
|
|
1204
|
-
const [userId, sessionId, patch
|
|
1612
|
+
const [userId, sessionId, patch] = args;
|
|
1205
1613
|
if (!userId || !sessionId) return null;
|
|
1206
1614
|
return {
|
|
1207
1615
|
type: "update",
|
|
1208
1616
|
userId,
|
|
1209
1617
|
sessionId,
|
|
1210
1618
|
patch,
|
|
1211
|
-
ttl,
|
|
1212
1619
|
timestamp
|
|
1213
1620
|
};
|
|
1214
1621
|
}
|
|
@@ -1404,7 +1811,6 @@ var StorageOAuthClientProvider = class {
|
|
|
1404
1811
|
__publicField(this, "_authUrl");
|
|
1405
1812
|
__publicField(this, "_clientId");
|
|
1406
1813
|
__publicField(this, "onRedirectCallback");
|
|
1407
|
-
__publicField(this, "tokenExpiresAt");
|
|
1408
1814
|
this.userId = options.userId;
|
|
1409
1815
|
this.serverId = options.serverId;
|
|
1410
1816
|
this.sessionId = options.sessionId;
|
|
@@ -1438,30 +1844,30 @@ var StorageOAuthClientProvider = class {
|
|
|
1438
1844
|
this._clientId = clientId_;
|
|
1439
1845
|
}
|
|
1440
1846
|
/**
|
|
1441
|
-
* Loads OAuth
|
|
1847
|
+
* Loads OAuth credentials from the session store
|
|
1442
1848
|
* @private
|
|
1443
1849
|
*/
|
|
1444
|
-
async
|
|
1445
|
-
const data = await sessions.
|
|
1850
|
+
async getCredentials() {
|
|
1851
|
+
const data = await sessions.getCredentials(this.userId, this.sessionId);
|
|
1446
1852
|
if (!data) {
|
|
1447
|
-
return {};
|
|
1853
|
+
return { userId: this.userId, sessionId: this.sessionId };
|
|
1448
1854
|
}
|
|
1449
1855
|
return data;
|
|
1450
1856
|
}
|
|
1451
1857
|
/**
|
|
1452
|
-
* Saves OAuth
|
|
1453
|
-
* @param data - Partial OAuth
|
|
1858
|
+
* Saves OAuth credentials to the session store
|
|
1859
|
+
* @param data - Partial OAuth credentials to save
|
|
1454
1860
|
* @private
|
|
1455
1861
|
* @throws Error if session doesn't exist (session must be created by controller layer)
|
|
1456
1862
|
*/
|
|
1457
|
-
async
|
|
1458
|
-
await sessions.
|
|
1863
|
+
async patchCredentials(data) {
|
|
1864
|
+
await sessions.patchCredentials(this.userId, this.sessionId, data);
|
|
1459
1865
|
}
|
|
1460
1866
|
/**
|
|
1461
1867
|
* Retrieves stored OAuth client information
|
|
1462
1868
|
*/
|
|
1463
1869
|
async clientInformation() {
|
|
1464
|
-
const data = await this.
|
|
1870
|
+
const data = await this.getCredentials();
|
|
1465
1871
|
if (data.clientId && !this._clientId) {
|
|
1466
1872
|
this._clientId = data.clientId;
|
|
1467
1873
|
}
|
|
@@ -1480,7 +1886,7 @@ var StorageOAuthClientProvider = class {
|
|
|
1480
1886
|
* Stores OAuth client information
|
|
1481
1887
|
*/
|
|
1482
1888
|
async saveClientInformation(clientInformation) {
|
|
1483
|
-
await this.
|
|
1889
|
+
await this.patchCredentials({
|
|
1484
1890
|
clientInformation,
|
|
1485
1891
|
clientId: clientInformation.client_id
|
|
1486
1892
|
});
|
|
@@ -1490,29 +1896,58 @@ var StorageOAuthClientProvider = class {
|
|
|
1490
1896
|
* Stores OAuth tokens
|
|
1491
1897
|
*/
|
|
1492
1898
|
async saveTokens(tokens) {
|
|
1493
|
-
|
|
1494
|
-
if (tokens.expires_in) {
|
|
1495
|
-
this.tokenExpiresAt = Date.now() + tokens.expires_in * 1e3 - TOKEN_EXPIRY_BUFFER_MS;
|
|
1496
|
-
}
|
|
1497
|
-
await this.saveSessionData(data);
|
|
1899
|
+
await this.patchCredentials({ tokens });
|
|
1498
1900
|
}
|
|
1499
1901
|
get authUrl() {
|
|
1500
1902
|
return this._authUrl;
|
|
1501
1903
|
}
|
|
1502
1904
|
async state() {
|
|
1503
|
-
|
|
1905
|
+
const nonce = nanoid(32);
|
|
1906
|
+
await this.patchCredentials({
|
|
1907
|
+
oauthState: {
|
|
1908
|
+
nonce,
|
|
1909
|
+
sessionId: this.sessionId,
|
|
1910
|
+
serverId: this.serverId,
|
|
1911
|
+
createdAt: Date.now()
|
|
1912
|
+
},
|
|
1913
|
+
codeVerifier: null
|
|
1914
|
+
});
|
|
1915
|
+
return formatOAuthState(nonce, this.sessionId);
|
|
1504
1916
|
}
|
|
1505
|
-
async checkState(
|
|
1506
|
-
const
|
|
1917
|
+
async checkState(state) {
|
|
1918
|
+
const parsed = parseOAuthState(state);
|
|
1919
|
+
if (!parsed) {
|
|
1920
|
+
return { valid: false, error: "Invalid OAuth state" };
|
|
1921
|
+
}
|
|
1922
|
+
if (parsed.sessionId !== this.sessionId) {
|
|
1923
|
+
return { valid: false, error: "OAuth state mismatch" };
|
|
1924
|
+
}
|
|
1925
|
+
const data = await sessions.getCredentials(this.userId, parsed.sessionId);
|
|
1507
1926
|
if (!data) {
|
|
1508
1927
|
return { valid: false, error: "Session not found" };
|
|
1509
1928
|
}
|
|
1510
|
-
|
|
1929
|
+
const oauthState = data.oauthState;
|
|
1930
|
+
if (!oauthState) {
|
|
1931
|
+
return { valid: false, error: "OAuth state not found" };
|
|
1932
|
+
}
|
|
1933
|
+
if (oauthState.nonce !== parsed.nonce || oauthState.sessionId !== parsed.sessionId || oauthState.serverId !== this.serverId) {
|
|
1934
|
+
return { valid: false, error: "OAuth state mismatch" };
|
|
1935
|
+
}
|
|
1936
|
+
if (Date.now() - oauthState.createdAt > STATE_EXPIRATION_MS) {
|
|
1937
|
+
return { valid: false, error: "OAuth state expired" };
|
|
1938
|
+
}
|
|
1939
|
+
return { valid: true, serverId: oauthState.serverId };
|
|
1511
1940
|
}
|
|
1512
|
-
async consumeState(
|
|
1941
|
+
async consumeState(state) {
|
|
1942
|
+
const result = await this.checkState(state);
|
|
1943
|
+
if (!result.valid) {
|
|
1944
|
+
throw new Error(result.error || "Invalid OAuth state");
|
|
1945
|
+
}
|
|
1946
|
+
await this.patchCredentials({ oauthState: null });
|
|
1513
1947
|
}
|
|
1514
1948
|
async redirectToAuthorization(authUrl) {
|
|
1515
1949
|
this._authUrl = authUrl.toString();
|
|
1950
|
+
await sessions.update(this.userId, this.sessionId, { authUrl: this._authUrl });
|
|
1516
1951
|
if (this.onRedirectCallback) {
|
|
1517
1952
|
this.onRedirectCallback(authUrl.toString());
|
|
1518
1953
|
}
|
|
@@ -1523,21 +1958,25 @@ var StorageOAuthClientProvider = class {
|
|
|
1523
1958
|
} else {
|
|
1524
1959
|
const updates = {};
|
|
1525
1960
|
if (scope === "client") {
|
|
1526
|
-
updates.clientInformation =
|
|
1527
|
-
updates.clientId =
|
|
1961
|
+
updates.clientInformation = null;
|
|
1962
|
+
updates.clientId = null;
|
|
1528
1963
|
} else if (scope === "tokens") {
|
|
1529
|
-
updates.tokens =
|
|
1964
|
+
updates.tokens = null;
|
|
1530
1965
|
} else if (scope === "verifier") {
|
|
1531
|
-
updates.codeVerifier =
|
|
1966
|
+
updates.codeVerifier = null;
|
|
1532
1967
|
}
|
|
1533
|
-
await this.
|
|
1968
|
+
await this.patchCredentials(updates);
|
|
1534
1969
|
}
|
|
1535
1970
|
}
|
|
1536
1971
|
async saveCodeVerifier(verifier) {
|
|
1537
|
-
await this.
|
|
1972
|
+
const data = await this.getCredentials();
|
|
1973
|
+
if (data.codeVerifier) {
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
await this.patchCredentials({ codeVerifier: verifier });
|
|
1538
1977
|
}
|
|
1539
1978
|
async codeVerifier() {
|
|
1540
|
-
const data = await this.
|
|
1979
|
+
const data = await this.getCredentials();
|
|
1541
1980
|
if (data.clientId && !this._clientId) {
|
|
1542
1981
|
this._clientId = data.clientId;
|
|
1543
1982
|
}
|
|
@@ -1547,23 +1986,14 @@ var StorageOAuthClientProvider = class {
|
|
|
1547
1986
|
return data.codeVerifier;
|
|
1548
1987
|
}
|
|
1549
1988
|
async deleteCodeVerifier() {
|
|
1550
|
-
await this.
|
|
1989
|
+
await this.patchCredentials({ codeVerifier: null });
|
|
1551
1990
|
}
|
|
1552
1991
|
async tokens() {
|
|
1553
|
-
const data = await this.
|
|
1992
|
+
const data = await this.getCredentials();
|
|
1554
1993
|
if (data.clientId && !this._clientId) {
|
|
1555
1994
|
this._clientId = data.clientId;
|
|
1556
1995
|
}
|
|
1557
|
-
return data.tokens;
|
|
1558
|
-
}
|
|
1559
|
-
isTokenExpired() {
|
|
1560
|
-
if (!this.tokenExpiresAt) {
|
|
1561
|
-
return false;
|
|
1562
|
-
}
|
|
1563
|
-
return Date.now() >= this.tokenExpiresAt;
|
|
1564
|
-
}
|
|
1565
|
-
setTokenExpiresAt(expiresAt) {
|
|
1566
|
-
this.tokenExpiresAt = expiresAt;
|
|
1996
|
+
return data.tokens ?? void 0;
|
|
1567
1997
|
}
|
|
1568
1998
|
};
|
|
1569
1999
|
|
|
@@ -1642,14 +2072,7 @@ var RpcErrorCodes = {
|
|
|
1642
2072
|
EXECUTION_ERROR: "EXECUTION_ERROR"};
|
|
1643
2073
|
|
|
1644
2074
|
// src/server/mcp/oauth-client.ts
|
|
1645
|
-
|
|
1646
|
-
if (!(error instanceof Error)) {
|
|
1647
|
-
return false;
|
|
1648
|
-
}
|
|
1649
|
-
const text = `${error.name} ${error.message}`.toLowerCase();
|
|
1650
|
-
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);
|
|
1651
|
-
}
|
|
1652
|
-
var MCPClient = class _MCPClient {
|
|
2075
|
+
var MCPClient = class {
|
|
1653
2076
|
/**
|
|
1654
2077
|
* Creates a new MCP client instance
|
|
1655
2078
|
* Can be initialized with minimal options (userId + sessionId) for session restoration
|
|
@@ -1824,17 +2247,18 @@ var MCPClient = class _MCPClient {
|
|
|
1824
2247
|
}
|
|
1825
2248
|
this.emitStateChange("INITIALIZING");
|
|
1826
2249
|
this.emitProgress("Loading session configuration...");
|
|
2250
|
+
let existingSession = null;
|
|
1827
2251
|
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
1828
|
-
|
|
1829
|
-
if (!
|
|
2252
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2253
|
+
if (!existingSession) {
|
|
1830
2254
|
throw new Error(`Session not found: ${this.sessionId}`);
|
|
1831
2255
|
}
|
|
1832
|
-
this.serverUrl = this.serverUrl ||
|
|
1833
|
-
this.callbackUrl = this.callbackUrl ||
|
|
1834
|
-
this.serverName = this.serverName ||
|
|
1835
|
-
this.serverId = this.serverId ||
|
|
1836
|
-
this.headers = this.headers ||
|
|
1837
|
-
this.createdAt =
|
|
2256
|
+
this.serverUrl = this.serverUrl || existingSession.serverUrl;
|
|
2257
|
+
this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
|
|
2258
|
+
this.serverName = this.serverName || existingSession.serverName;
|
|
2259
|
+
this.serverId = this.serverId || existingSession.serverId || "unknown";
|
|
2260
|
+
this.headers = this.headers || existingSession.headers;
|
|
2261
|
+
this.createdAt = existingSession.createdAt;
|
|
1838
2262
|
}
|
|
1839
2263
|
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
1840
2264
|
throw new Error("Missing required connection metadata");
|
|
@@ -1878,10 +2302,13 @@ var MCPClient = class _MCPClient {
|
|
|
1878
2302
|
}
|
|
1879
2303
|
);
|
|
1880
2304
|
}
|
|
1881
|
-
|
|
2305
|
+
if (existingSession === null) {
|
|
2306
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2307
|
+
}
|
|
1882
2308
|
if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
|
|
1883
2309
|
this.createdAt = Date.now();
|
|
1884
|
-
|
|
2310
|
+
const updatedAt = this.createdAt;
|
|
2311
|
+
console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
|
|
1885
2312
|
await sessions.create({
|
|
1886
2313
|
sessionId: this.sessionId,
|
|
1887
2314
|
userId: this.userId,
|
|
@@ -1892,18 +2319,18 @@ var MCPClient = class _MCPClient {
|
|
|
1892
2319
|
transportType: this.transportType || "streamable-http",
|
|
1893
2320
|
headers: this.headers,
|
|
1894
2321
|
createdAt: this.createdAt,
|
|
1895
|
-
|
|
1896
|
-
|
|
2322
|
+
updatedAt,
|
|
2323
|
+
status: "pending"
|
|
2324
|
+
});
|
|
1897
2325
|
}
|
|
1898
2326
|
}
|
|
1899
2327
|
/**
|
|
1900
2328
|
* Saves current session state to the session store
|
|
1901
2329
|
* Creates new session if it doesn't exist, updates if it does
|
|
1902
|
-
* @param
|
|
1903
|
-
* @param active - Session status marker used to avoid unnecessary TTL rewrites
|
|
2330
|
+
* @param status - Session lifecycle status used by storage cleanup
|
|
1904
2331
|
* @private
|
|
1905
2332
|
*/
|
|
1906
|
-
async saveSession(
|
|
2333
|
+
async saveSession(status = "active", existingSession) {
|
|
1907
2334
|
if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
|
|
1908
2335
|
return;
|
|
1909
2336
|
}
|
|
@@ -1917,13 +2344,29 @@ var MCPClient = class _MCPClient {
|
|
|
1917
2344
|
transportType: this.transportType || "streamable-http",
|
|
1918
2345
|
headers: this.headers,
|
|
1919
2346
|
createdAt: this.createdAt || Date.now(),
|
|
1920
|
-
|
|
2347
|
+
updatedAt: Date.now(),
|
|
2348
|
+
status
|
|
1921
2349
|
};
|
|
1922
|
-
|
|
2350
|
+
if (status === "active") {
|
|
2351
|
+
sessionData.authUrl = null;
|
|
2352
|
+
}
|
|
2353
|
+
if (existingSession === void 0) {
|
|
2354
|
+
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2355
|
+
}
|
|
1923
2356
|
if (existingSession) {
|
|
1924
|
-
await sessions.update(this.userId, this.sessionId, sessionData
|
|
2357
|
+
await sessions.update(this.userId, this.sessionId, sessionData);
|
|
1925
2358
|
} else {
|
|
1926
|
-
await sessions.create(sessionData
|
|
2359
|
+
await sessions.create(sessionData);
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
/**
|
|
2363
|
+
* Removes transient setup/auth sessions without masking the original error.
|
|
2364
|
+
* @private
|
|
2365
|
+
*/
|
|
2366
|
+
async deleteTransientSession() {
|
|
2367
|
+
try {
|
|
2368
|
+
await sessions.delete(this.userId, this.sessionId);
|
|
2369
|
+
} catch {
|
|
1927
2370
|
}
|
|
1928
2371
|
}
|
|
1929
2372
|
/**
|
|
@@ -1983,15 +2426,13 @@ var MCPClient = class _MCPClient {
|
|
|
1983
2426
|
throw new Error(error);
|
|
1984
2427
|
}
|
|
1985
2428
|
try {
|
|
1986
|
-
this.emitProgress("Validating OAuth tokens...");
|
|
1987
|
-
await this.getValidTokens();
|
|
1988
2429
|
this.emitStateChange("CONNECTING");
|
|
1989
2430
|
const { transportType } = await this.tryConnect();
|
|
1990
2431
|
this.transportType = transportType;
|
|
1991
2432
|
this.emitStateChange("CONNECTED");
|
|
1992
2433
|
this.emitProgress("Connected successfully");
|
|
1993
|
-
console.log(`[MCPClient] Saving session ${this.sessionId}
|
|
1994
|
-
await this.saveSession(
|
|
2434
|
+
console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
|
|
2435
|
+
await this.saveSession("active");
|
|
1995
2436
|
} catch (error) {
|
|
1996
2437
|
if (error instanceof UnauthorizedError$1 || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
|
|
1997
2438
|
let authUrl = "";
|
|
@@ -2003,15 +2444,12 @@ var MCPClient = class _MCPClient {
|
|
|
2003
2444
|
const message = detail.toLowerCase() === "unauthorized" ? "OAuth authorization URL not available" : `OAuth authorization URL not available: ${detail}`;
|
|
2004
2445
|
this.emitError(message, "auth");
|
|
2005
2446
|
this.emitStateChange("FAILED");
|
|
2006
|
-
|
|
2007
|
-
await sessions.delete(this.userId, this.sessionId);
|
|
2008
|
-
} catch {
|
|
2009
|
-
}
|
|
2447
|
+
await this.deleteTransientSession();
|
|
2010
2448
|
throw new Error(message);
|
|
2011
2449
|
}
|
|
2012
2450
|
this.emitStateChange("AUTHENTICATING");
|
|
2013
|
-
console.log(`[MCPClient] Saving session ${this.sessionId}
|
|
2014
|
-
await this.saveSession(
|
|
2451
|
+
console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
|
|
2452
|
+
await this.saveSession("pending");
|
|
2015
2453
|
if (this.serverId) {
|
|
2016
2454
|
this._onConnectionEvent.fire({
|
|
2017
2455
|
type: "auth_required",
|
|
@@ -2031,7 +2469,7 @@ var MCPClient = class _MCPClient {
|
|
|
2031
2469
|
this.emitStateChange("FAILED");
|
|
2032
2470
|
try {
|
|
2033
2471
|
const existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2034
|
-
if (!existingSession || existingSession.
|
|
2472
|
+
if (!existingSession || existingSession.status !== "active") {
|
|
2035
2473
|
await sessions.delete(this.userId, this.sessionId);
|
|
2036
2474
|
}
|
|
2037
2475
|
} catch {
|
|
@@ -2046,7 +2484,7 @@ var MCPClient = class _MCPClient {
|
|
|
2046
2484
|
* @param authCode - Authorization code received from OAuth callback
|
|
2047
2485
|
*/
|
|
2048
2486
|
// TODO: needs to be optimized
|
|
2049
|
-
async finishAuth(authCode) {
|
|
2487
|
+
async finishAuth(authCode, state) {
|
|
2050
2488
|
this.emitStateChange("AUTHENTICATING");
|
|
2051
2489
|
this.emitProgress("Exchanging authorization code for tokens...");
|
|
2052
2490
|
await this.initialize();
|
|
@@ -2056,6 +2494,16 @@ var MCPClient = class _MCPClient {
|
|
|
2056
2494
|
this.emitStateChange("FAILED");
|
|
2057
2495
|
throw new Error(error);
|
|
2058
2496
|
}
|
|
2497
|
+
if (state) {
|
|
2498
|
+
const stateCheck = await this.oauthProvider.checkState(state);
|
|
2499
|
+
if (!stateCheck.valid) {
|
|
2500
|
+
const error = stateCheck.error || "Invalid OAuth state";
|
|
2501
|
+
this.emitError(error, "auth");
|
|
2502
|
+
this.emitStateChange("FAILED");
|
|
2503
|
+
throw new Error(error);
|
|
2504
|
+
}
|
|
2505
|
+
await this.oauthProvider.consumeState(state);
|
|
2506
|
+
}
|
|
2059
2507
|
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2060
2508
|
let lastError;
|
|
2061
2509
|
let tokensExchanged = false;
|
|
@@ -2095,8 +2543,8 @@ var MCPClient = class _MCPClient {
|
|
|
2095
2543
|
await this.client.connect(this.transport);
|
|
2096
2544
|
this.transportType = currentType;
|
|
2097
2545
|
this.emitStateChange("CONNECTED");
|
|
2098
|
-
console.log(`[MCPClient]
|
|
2099
|
-
await this.saveSession(
|
|
2546
|
+
console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
|
|
2547
|
+
await this.saveSession("active");
|
|
2100
2548
|
return;
|
|
2101
2549
|
} catch (error) {
|
|
2102
2550
|
lastError = error;
|
|
@@ -2109,12 +2557,14 @@ var MCPClient = class _MCPClient {
|
|
|
2109
2557
|
const msg = error instanceof Error ? error.message : "Authentication failed";
|
|
2110
2558
|
this.emitError(msg, "auth");
|
|
2111
2559
|
this.emitStateChange("FAILED");
|
|
2560
|
+
await this.deleteTransientSession();
|
|
2112
2561
|
throw error;
|
|
2113
2562
|
}
|
|
2114
2563
|
if (isLastAttempt) {
|
|
2115
2564
|
const msg = error instanceof Error ? error.message : "Authentication failed";
|
|
2116
2565
|
this.emitError(msg, "auth");
|
|
2117
2566
|
this.emitStateChange("FAILED");
|
|
2567
|
+
await this.deleteTransientSession();
|
|
2118
2568
|
throw error;
|
|
2119
2569
|
}
|
|
2120
2570
|
this.emitProgress(`Auth attempt with ${currentType} failed: ${errorMessage}. Retrying...`);
|
|
@@ -2124,6 +2574,7 @@ var MCPClient = class _MCPClient {
|
|
|
2124
2574
|
const errorMessage = lastError instanceof Error ? lastError.message : "Authentication failed";
|
|
2125
2575
|
this.emitError(errorMessage, "auth");
|
|
2126
2576
|
this.emitStateChange("FAILED");
|
|
2577
|
+
await this.deleteTransientSession();
|
|
2127
2578
|
throw lastError;
|
|
2128
2579
|
}
|
|
2129
2580
|
}
|
|
@@ -2309,67 +2760,6 @@ var MCPClient = class _MCPClient {
|
|
|
2309
2760
|
};
|
|
2310
2761
|
return await this.client.request(request, ReadResourceResultSchema);
|
|
2311
2762
|
}
|
|
2312
|
-
/**
|
|
2313
|
-
* Refreshes the OAuth access token using the refresh token
|
|
2314
|
-
* Discovers OAuth metadata from server and exchanges refresh token for new access token
|
|
2315
|
-
* @returns True if refresh was successful, false otherwise
|
|
2316
|
-
*/
|
|
2317
|
-
async refreshToken() {
|
|
2318
|
-
await this.initialize();
|
|
2319
|
-
if (!this.oauthProvider) {
|
|
2320
|
-
return false;
|
|
2321
|
-
}
|
|
2322
|
-
const tokens = await this.oauthProvider.tokens();
|
|
2323
|
-
if (!tokens || !tokens.refresh_token) {
|
|
2324
|
-
return false;
|
|
2325
|
-
}
|
|
2326
|
-
const clientInformation = await this.oauthProvider.clientInformation();
|
|
2327
|
-
if (!clientInformation) {
|
|
2328
|
-
return false;
|
|
2329
|
-
}
|
|
2330
|
-
try {
|
|
2331
|
-
const resourceMetadata = await discoverOAuthProtectedResourceMetadata(this.serverUrl);
|
|
2332
|
-
const authServerUrl = resourceMetadata?.authorization_servers?.[0] || this.serverUrl;
|
|
2333
|
-
const authMetadata = await discoverAuthorizationServerMetadata(authServerUrl);
|
|
2334
|
-
const newTokens = await refreshAuthorization(authServerUrl, {
|
|
2335
|
-
metadata: authMetadata,
|
|
2336
|
-
clientInformation,
|
|
2337
|
-
refreshToken: tokens.refresh_token
|
|
2338
|
-
});
|
|
2339
|
-
await this.oauthProvider.saveTokens(newTokens);
|
|
2340
|
-
return true;
|
|
2341
|
-
} catch (error) {
|
|
2342
|
-
console.error("[OAuth] Token refresh failed:", error);
|
|
2343
|
-
if (isInvalidRefreshTokenError(error)) {
|
|
2344
|
-
try {
|
|
2345
|
-
await this.oauthProvider.invalidateCredentials?.("tokens");
|
|
2346
|
-
this.emitProgress("OAuth refresh token is invalid; requesting reauthorization...");
|
|
2347
|
-
} catch (invalidateError) {
|
|
2348
|
-
console.warn("[OAuth] Failed to invalidate stale refresh token credentials:", invalidateError);
|
|
2349
|
-
}
|
|
2350
|
-
}
|
|
2351
|
-
return false;
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
/**
|
|
2355
|
-
* Ensures OAuth tokens are valid, refreshing them if expired
|
|
2356
|
-
* Called automatically by connect() - rarely needs to be called manually
|
|
2357
|
-
* @returns True if valid tokens are available, false otherwise
|
|
2358
|
-
*/
|
|
2359
|
-
async getValidTokens() {
|
|
2360
|
-
await this.initialize();
|
|
2361
|
-
if (!this.oauthProvider) {
|
|
2362
|
-
return false;
|
|
2363
|
-
}
|
|
2364
|
-
const tokens = await this.oauthProvider.tokens();
|
|
2365
|
-
if (!tokens) {
|
|
2366
|
-
return false;
|
|
2367
|
-
}
|
|
2368
|
-
if (this.oauthProvider.isTokenExpired()) {
|
|
2369
|
-
return await this.refreshToken();
|
|
2370
|
-
}
|
|
2371
|
-
return true;
|
|
2372
|
-
}
|
|
2373
2763
|
/**
|
|
2374
2764
|
* Reconnects to MCP server using existing OAuth provider from Redis
|
|
2375
2765
|
* Used for session restoration in serverless environments
|
|
@@ -2499,68 +2889,6 @@ var MCPClient = class _MCPClient {
|
|
|
2499
2889
|
getSessionId() {
|
|
2500
2890
|
return this.sessionId;
|
|
2501
2891
|
}
|
|
2502
|
-
/**
|
|
2503
|
-
* Gets MCP server configuration for all active user sessions
|
|
2504
|
-
* Loads sessions from Redis, validates OAuth tokens, refreshes if expired
|
|
2505
|
-
* Returns ready-to-use configuration with valid auth headers
|
|
2506
|
-
* @param userId - User ID to fetch sessions for
|
|
2507
|
-
* @returns Object keyed by sanitized server labels containing transport, url, headers, etc.
|
|
2508
|
-
* @static
|
|
2509
|
-
*/
|
|
2510
|
-
static async getMcpServerConfig(userId) {
|
|
2511
|
-
const mcpConfig = {};
|
|
2512
|
-
const sessionList = await sessions.list(userId);
|
|
2513
|
-
await Promise.all(
|
|
2514
|
-
sessionList.map(async (sessionData) => {
|
|
2515
|
-
const { sessionId } = sessionData;
|
|
2516
|
-
try {
|
|
2517
|
-
if (!sessionData.serverId || !sessionData.transportType || !sessionData.serverUrl || !sessionData.callbackUrl) {
|
|
2518
|
-
await sessions.delete(userId, sessionId);
|
|
2519
|
-
return;
|
|
2520
|
-
}
|
|
2521
|
-
let headers;
|
|
2522
|
-
try {
|
|
2523
|
-
const client = new _MCPClient({
|
|
2524
|
-
userId,
|
|
2525
|
-
sessionId,
|
|
2526
|
-
serverId: sessionData.serverId,
|
|
2527
|
-
serverUrl: sessionData.serverUrl,
|
|
2528
|
-
callbackUrl: sessionData.callbackUrl,
|
|
2529
|
-
serverName: sessionData.serverName,
|
|
2530
|
-
transportType: sessionData.transportType,
|
|
2531
|
-
headers: sessionData.headers
|
|
2532
|
-
});
|
|
2533
|
-
await client.initialize();
|
|
2534
|
-
const hasValidTokens = await client.getValidTokens();
|
|
2535
|
-
if (hasValidTokens && client.oauthProvider) {
|
|
2536
|
-
const tokens = await client.oauthProvider.tokens();
|
|
2537
|
-
if (tokens?.access_token) {
|
|
2538
|
-
headers = { Authorization: `Bearer ${tokens.access_token}` };
|
|
2539
|
-
}
|
|
2540
|
-
}
|
|
2541
|
-
} catch (error) {
|
|
2542
|
-
console.warn(`[MCP] Failed to get OAuth tokens for ${sessionId}:`, error);
|
|
2543
|
-
}
|
|
2544
|
-
const label = sanitizeServerLabel(
|
|
2545
|
-
sessionData.serverName || sessionData.serverId || "server"
|
|
2546
|
-
);
|
|
2547
|
-
mcpConfig[label] = {
|
|
2548
|
-
transport: sessionData.transportType,
|
|
2549
|
-
url: sessionData.serverUrl,
|
|
2550
|
-
...sessionData.serverName && {
|
|
2551
|
-
serverName: sessionData.serverName,
|
|
2552
|
-
serverLabel: label
|
|
2553
|
-
},
|
|
2554
|
-
...headers && { headers }
|
|
2555
|
-
};
|
|
2556
|
-
} catch (error) {
|
|
2557
|
-
await sessions.delete(userId, sessionId);
|
|
2558
|
-
console.warn(`[MCP] Failed to process session ${sessionId}:`, error);
|
|
2559
|
-
}
|
|
2560
|
-
})
|
|
2561
|
-
);
|
|
2562
|
-
return mcpConfig;
|
|
2563
|
-
}
|
|
2564
2892
|
};
|
|
2565
2893
|
|
|
2566
2894
|
// src/server/mcp/multi-session-client.ts
|
|
@@ -2595,18 +2923,13 @@ var MultiSessionClient = class {
|
|
|
2595
2923
|
*
|
|
2596
2924
|
* A session is considered connectable when:
|
|
2597
2925
|
* - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
|
|
2598
|
-
* - Its
|
|
2599
|
-
* either mid-OAuth flow, auth-pending, or previously failed. We skip those here
|
|
2926
|
+
* - Its status is `active`. Pending sessions are skipped here
|
|
2600
2927
|
* and let the OAuth flow complete separately before we try to reconnect them.
|
|
2601
|
-
*
|
|
2602
|
-
* Note: Sessions where `active` is `undefined` (legacy records) are included
|
|
2603
|
-
* for backwards compatibility.
|
|
2604
2928
|
*/
|
|
2605
2929
|
async getActiveSessions() {
|
|
2606
2930
|
const sessionList = await sessions.list(this.userId);
|
|
2607
2931
|
const valid = sessionList.filter(
|
|
2608
|
-
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.
|
|
2609
|
-
// exclude OAuth-pending / failed sessions
|
|
2932
|
+
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
|
|
2610
2933
|
);
|
|
2611
2934
|
return valid;
|
|
2612
2935
|
}
|
|
@@ -2877,7 +3200,8 @@ var SSEConnectionManager = class {
|
|
|
2877
3200
|
serverUrl: s.serverUrl,
|
|
2878
3201
|
transport: s.transportType,
|
|
2879
3202
|
createdAt: s.createdAt,
|
|
2880
|
-
|
|
3203
|
+
updatedAt: s.updatedAt ?? s.createdAt,
|
|
3204
|
+
status: s.status ?? "pending"
|
|
2881
3205
|
}))
|
|
2882
3206
|
};
|
|
2883
3207
|
}
|
|
@@ -2893,7 +3217,7 @@ var SSEConnectionManager = class {
|
|
|
2893
3217
|
(s) => s.serverId === serverId || s.serverUrl === serverUrl
|
|
2894
3218
|
);
|
|
2895
3219
|
if (duplicate) {
|
|
2896
|
-
if (duplicate.
|
|
3220
|
+
if (duplicate.status === "pending") {
|
|
2897
3221
|
await this.getSession({ sessionId: duplicate.sessionId });
|
|
2898
3222
|
return {
|
|
2899
3223
|
sessionId: duplicate.sessionId,
|
|
@@ -3080,7 +3404,10 @@ var SSEConnectionManager = class {
|
|
|
3080
3404
|
* Complete OAuth authorization flow
|
|
3081
3405
|
*/
|
|
3082
3406
|
async finishAuth(params) {
|
|
3083
|
-
const {
|
|
3407
|
+
const { code } = params;
|
|
3408
|
+
const oauthState = params.state;
|
|
3409
|
+
const parsedState = parseOAuthState(oauthState);
|
|
3410
|
+
const sessionId = parsedState?.sessionId || oauthState;
|
|
3084
3411
|
const session = await sessions.get(this.userId, sessionId);
|
|
3085
3412
|
if (!session) {
|
|
3086
3413
|
throw new Error("Session not found");
|
|
@@ -3105,7 +3432,7 @@ var SSEConnectionManager = class {
|
|
|
3105
3432
|
headers: session.headers
|
|
3106
3433
|
});
|
|
3107
3434
|
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
3108
|
-
await client.finishAuth(code);
|
|
3435
|
+
await client.finishAuth(code, oauthState);
|
|
3109
3436
|
this.clients.set(sessionId, client);
|
|
3110
3437
|
const tools = await client.listTools();
|
|
3111
3438
|
return { success: true, toolCount: tools.tools.length };
|