@mcp-ts/sdk 2.5.3 → 2.6.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 +3 -574
- package/dist/adapters/agui-adapter.d.mts +4 -4
- package/dist/adapters/agui-adapter.d.ts +4 -4
- package/dist/adapters/agui-middleware.d.mts +4 -4
- package/dist/adapters/agui-middleware.d.ts +4 -4
- package/dist/adapters/ai-adapter.d.mts +4 -4
- package/dist/adapters/ai-adapter.d.ts +4 -4
- package/dist/adapters/langchain-adapter.d.mts +4 -4
- package/dist/adapters/langchain-adapter.d.ts +4 -4
- package/dist/adapters/mastra-adapter.d.mts +3 -3
- package/dist/adapters/mastra-adapter.d.ts +3 -3
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +3 -0
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +10 -18
- package/dist/client/react.d.ts +10 -18
- package/dist/client/react.js +17 -0
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +17 -0
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +4 -4
- package/dist/client/vue.d.ts +4 -4
- package/dist/client/vue.js +3 -0
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +3 -0
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{index-BRZO_JyI.d.mts → index-BrDkbut5.d.ts} +2 -1
- package/dist/{index-C3YGagi2.d.ts → index-DHQvlx4K.d.mts} +2 -1
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1056 -876
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1048 -871
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-CuacvZaQ.d.ts → multi-session-client-CQzymvHl.d.ts} +183 -227
- package/dist/{multi-session-client-DqzT8Oo7.d.mts → multi-session-client-C_Ja2TuV.d.mts} +183 -227
- package/dist/server/index.d.mts +338 -88
- package/dist/server/index.d.ts +338 -88
- package/dist/server/index.js +1053 -876
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +1045 -871
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +4 -4
- package/dist/shared/index.d.ts +4 -4
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{tool-router-B0qpui-V.d.mts → tool-router-C1n7OXbl.d.mts} +1 -1
- package/dist/{tool-router-2qUyZ-cP.d.ts → tool-router-CBIawFnt.d.ts} +1 -1
- package/dist/{types-BkJ_rgzo.d.mts → types-DX71u9gR.d.mts} +11 -5
- package/dist/{types-BkJ_rgzo.d.ts → types-DX71u9gR.d.ts} +11 -5
- package/migrations/neon/20260513010000_install_mcp_sessions.sql +4 -34
- package/migrations/supabase/20260330195700_install_mcp_sessions.sql +5 -62
- package/package.json +2 -1
- package/src/client/core/sse-client.ts +5 -0
- package/src/client/react/use-mcp.ts +27 -28
- package/src/server/handlers/sse-handler.ts +671 -633
- package/src/server/index.ts +2 -1
- package/src/server/mcp/multi-session-client.ts +34 -10
- package/src/server/mcp/oauth-client.ts +197 -205
- package/src/server/mcp/storage-oauth-provider.ts +148 -41
- package/src/server/mcp/tool-policy-gateway.ts +11 -1
- package/src/server/storage/file-backend.ts +25 -21
- package/src/server/storage/index.ts +71 -0
- package/src/server/storage/memory-backend.ts +24 -12
- package/src/server/storage/neon-backend.ts +70 -72
- package/src/server/storage/redis-backend.ts +26 -36
- package/src/server/storage/sqlite-backend.ts +25 -39
- package/src/server/storage/supabase-backend.ts +45 -54
- package/src/server/storage/types.ts +22 -2
- package/src/shared/types.ts +13 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SessionStore, Session, SessionCredentials } from './types.js';
|
|
1
|
+
import type { SessionStore, Session, SessionCredentials, GetOptions, SessionResult } from './types.js';
|
|
2
2
|
import type { SessionStatus } from './types.js';
|
|
3
3
|
import { DORMANT_SESSION_EXPIRATION_MS } from '../../shared/constants.js';
|
|
4
4
|
import { generateSessionId } from '../../shared/utils.js';
|
|
@@ -9,7 +9,6 @@ import { normalizeToolPolicy } from './tool-policy.js';
|
|
|
9
9
|
export interface NeonStorageOptions {
|
|
10
10
|
schema?: string;
|
|
11
11
|
table?: string;
|
|
12
|
-
credentialsTable?: string;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
type NeonSql = {
|
|
@@ -32,21 +31,16 @@ type NeonSessionRow = {
|
|
|
32
31
|
auth_url?: string | null;
|
|
33
32
|
status?: SessionStatus | null;
|
|
34
33
|
tool_policy?: unknown;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
type NeonCredentialsRow = {
|
|
38
|
-
session_id: string;
|
|
39
|
-
user_id: string;
|
|
40
34
|
client_information?: unknown;
|
|
41
35
|
tokens?: unknown;
|
|
42
36
|
code_verifier?: unknown;
|
|
43
37
|
client_id?: string | null;
|
|
44
38
|
oauth_state?: unknown;
|
|
39
|
+
enabled?: boolean;
|
|
45
40
|
};
|
|
46
41
|
|
|
47
42
|
export class NeonStorageBackend implements SessionStore {
|
|
48
43
|
private readonly tableName: string;
|
|
49
|
-
private readonly credentialsTableName: string;
|
|
50
44
|
|
|
51
45
|
constructor(
|
|
52
46
|
private readonly sql: NeonSql,
|
|
@@ -54,14 +48,11 @@ export class NeonStorageBackend implements SessionStore {
|
|
|
54
48
|
) {
|
|
55
49
|
const schema = options.schema || 'public';
|
|
56
50
|
const table = options.table || 'mcp_sessions';
|
|
57
|
-
const credentialsTable = options.credentialsTable || 'mcp_credentials';
|
|
58
51
|
this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
|
|
59
|
-
this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
|
|
60
52
|
}
|
|
61
53
|
|
|
62
54
|
async init(): Promise<void> {
|
|
63
55
|
await this.assertTable(this.tableName, 'mcp_sessions');
|
|
64
|
-
await this.assertTable(this.credentialsTableName, 'mcp_credentials');
|
|
65
56
|
console.log('[mcp-ts][Storage] Neon: storage tables verified.');
|
|
66
57
|
}
|
|
67
58
|
|
|
@@ -106,18 +97,12 @@ export class NeonStorageBackend implements SessionStore {
|
|
|
106
97
|
authUrl: row.auth_url ?? undefined,
|
|
107
98
|
status: row.status ?? 'pending',
|
|
108
99
|
toolPolicy: normalizeToolPolicy(row.tool_policy as Parameters<typeof normalizeToolPolicy>[0]),
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private mapRowToCredentials(row: NeonCredentialsRow, userId: string, sessionId: string): SessionCredentials {
|
|
113
|
-
return {
|
|
114
|
-
sessionId,
|
|
115
|
-
userId,
|
|
116
100
|
clientInformation: decryptObject(row.client_information),
|
|
117
101
|
tokens: decryptObject(row.tokens),
|
|
118
102
|
codeVerifier: decryptObject(row.code_verifier),
|
|
119
103
|
clientId: row.client_id ?? undefined,
|
|
120
|
-
oauthState: row.oauth_state as
|
|
104
|
+
oauthState: row.oauth_state as Session['oauthState'],
|
|
105
|
+
enabled: row.enabled ?? true,
|
|
121
106
|
};
|
|
122
107
|
}
|
|
123
108
|
|
|
@@ -196,7 +181,8 @@ export class NeonStorageBackend implements SessionStore {
|
|
|
196
181
|
'status' in data ||
|
|
197
182
|
'headers' in data ||
|
|
198
183
|
'authUrl' in data ||
|
|
199
|
-
'toolPolicy' in data
|
|
184
|
+
'toolPolicy' in data ||
|
|
185
|
+
'enabled' in data
|
|
200
186
|
);
|
|
201
187
|
|
|
202
188
|
if (shouldUpdateSession) {
|
|
@@ -225,6 +211,10 @@ export class NeonStorageBackend implements SessionStore {
|
|
|
225
211
|
addSet('tool_policy', toolPolicy);
|
|
226
212
|
}
|
|
227
213
|
|
|
214
|
+
if ('enabled' in data) {
|
|
215
|
+
addSet('enabled', updatedSession.enabled);
|
|
216
|
+
}
|
|
217
|
+
|
|
228
218
|
setClauses.push('updated_at = now()');
|
|
229
219
|
|
|
230
220
|
const updatedRows = await this.sql.query(
|
|
@@ -244,53 +234,58 @@ export class NeonStorageBackend implements SessionStore {
|
|
|
244
234
|
|
|
245
235
|
async patchCredentials(userId: string, sessionId: string, data: Partial<SessionCredentials>): Promise<void> {
|
|
246
236
|
if (!this.hasCredentialData(data)) return;
|
|
237
|
+
const setClauses: string[] = [];
|
|
238
|
+
const values: unknown[] = [];
|
|
239
|
+
let paramIndex = 1;
|
|
247
240
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
'clientId' in data,
|
|
279
|
-
'oauthState' in data,
|
|
280
|
-
]
|
|
281
|
-
);
|
|
241
|
+
const addSet = (column: string, value: unknown) => {
|
|
242
|
+
setClauses.push(`${column} = $${paramIndex++}`);
|
|
243
|
+
values.push(value);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if ('clientInformation' in data) {
|
|
247
|
+
addSet('client_information', data.clientInformation == null ? null : encryptObject(data.clientInformation));
|
|
248
|
+
}
|
|
249
|
+
if ('tokens' in data) {
|
|
250
|
+
addSet('tokens', data.tokens == null ? null : encryptObject(data.tokens));
|
|
251
|
+
}
|
|
252
|
+
if ('codeVerifier' in data) {
|
|
253
|
+
addSet('code_verifier', data.codeVerifier == null ? null : encryptObject(data.codeVerifier));
|
|
254
|
+
}
|
|
255
|
+
if ('clientId' in data) {
|
|
256
|
+
addSet('client_id', data.clientId ?? null);
|
|
257
|
+
}
|
|
258
|
+
if ('oauthState' in data) {
|
|
259
|
+
addSet('oauth_state', data.oauthState ?? null);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
setClauses.push('updated_at = now()');
|
|
263
|
+
|
|
264
|
+
const updatedRows = await this.sql.query(
|
|
265
|
+
`UPDATE ${this.tableName}
|
|
266
|
+
SET ${setClauses.join(', ')}
|
|
267
|
+
WHERE user_id = $${paramIndex++} AND session_id = $${paramIndex++}
|
|
268
|
+
RETURNING id`,
|
|
269
|
+
[...values, userId, sessionId]
|
|
270
|
+
) as Array<{ id: string }>;
|
|
282
271
|
|
|
272
|
+
if (updatedRows.length === 0) {
|
|
273
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
274
|
+
}
|
|
283
275
|
}
|
|
284
276
|
|
|
285
|
-
async get(userId: string, sessionId: string): Promise<
|
|
277
|
+
async get(userId: string, sessionId: string, options?: GetOptions): Promise<SessionResult | null> {
|
|
286
278
|
try {
|
|
279
|
+
const selection = options?.includeCredentials
|
|
280
|
+
? '*'
|
|
281
|
+
: 'session_id, user_id, server_id, server_name, server_url, transport_type, callback_url, created_at, updated_at, expires_at, headers, auth_url, status, tool_policy, enabled';
|
|
282
|
+
|
|
287
283
|
const rows = await this.sql.query(
|
|
288
|
-
`SELECT
|
|
284
|
+
`SELECT ${selection} FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
289
285
|
[userId, sessionId]
|
|
290
286
|
) as NeonSessionRow[];
|
|
291
287
|
|
|
292
288
|
if (!rows[0]) return null;
|
|
293
|
-
|
|
294
289
|
return this.mapRowToSessionData(rows[0]);
|
|
295
290
|
} catch (error) {
|
|
296
291
|
console.error('[NeonStorage] Failed to get session:', error);
|
|
@@ -300,21 +295,23 @@ export class NeonStorageBackend implements SessionStore {
|
|
|
300
295
|
|
|
301
296
|
async getCredentials(userId: string, sessionId: string): Promise<SessionCredentials | null> {
|
|
302
297
|
try {
|
|
303
|
-
const
|
|
304
|
-
`SELECT
|
|
305
|
-
|
|
306
|
-
) as NeonCredentialsRow[];
|
|
307
|
-
|
|
308
|
-
if (credentialRows[0]) {
|
|
309
|
-
return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const sessionRows = await this.sql.query(
|
|
313
|
-
`SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
298
|
+
const rows = await this.sql.query(
|
|
299
|
+
`SELECT client_information, tokens, code_verifier, client_id, oauth_state
|
|
300
|
+
FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
314
301
|
[userId, sessionId]
|
|
315
|
-
) as
|
|
302
|
+
) as NeonSessionRow[];
|
|
316
303
|
|
|
317
|
-
|
|
304
|
+
if (!rows[0]) return null;
|
|
305
|
+
const row = rows[0];
|
|
306
|
+
return {
|
|
307
|
+
sessionId,
|
|
308
|
+
userId,
|
|
309
|
+
clientInformation: decryptObject(row.client_information),
|
|
310
|
+
tokens: decryptObject(row.tokens),
|
|
311
|
+
codeVerifier: decryptObject(row.code_verifier),
|
|
312
|
+
clientId: row.client_id ?? undefined,
|
|
313
|
+
oauthState: row.oauth_state as SessionCredentials['oauthState'],
|
|
314
|
+
};
|
|
318
315
|
} catch (error) {
|
|
319
316
|
console.error('[NeonStorage] Failed to get credentials:', error);
|
|
320
317
|
return null;
|
|
@@ -337,7 +334,9 @@ export class NeonStorageBackend implements SessionStore {
|
|
|
337
334
|
async clearCredentials(userId: string, sessionId: string): Promise<void> {
|
|
338
335
|
try {
|
|
339
336
|
await this.sql.query(
|
|
340
|
-
`
|
|
337
|
+
`UPDATE ${this.tableName}
|
|
338
|
+
SET client_information = null, tokens = null, code_verifier = null, client_id = null, oauth_state = null, updated_at = now()
|
|
339
|
+
WHERE user_id = $1 AND session_id = $2`,
|
|
341
340
|
[userId, sessionId]
|
|
342
341
|
);
|
|
343
342
|
} catch (error) {
|
|
@@ -383,7 +382,6 @@ export class NeonStorageBackend implements SessionStore {
|
|
|
383
382
|
|
|
384
383
|
async clearAll(): Promise<void> {
|
|
385
384
|
try {
|
|
386
|
-
await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
|
|
387
385
|
await this.sql.query(`DELETE FROM ${this.tableName}`);
|
|
388
386
|
} catch (error) {
|
|
389
387
|
console.error('[NeonStorage] Failed to clear sessions:', error);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Redis } from 'ioredis';
|
|
2
|
-
import type { SessionStore, Session, SessionCredentials } from './types.js';
|
|
2
|
+
import type { SessionStore, Session, SessionCredentials, GetOptions, SessionResult } from './types.js';
|
|
3
3
|
import { generateSessionId } from '../../shared/utils.js';
|
|
4
4
|
import {
|
|
5
5
|
mergeSessionUpdate,
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
*/
|
|
14
14
|
export class RedisStorageBackend implements SessionStore {
|
|
15
15
|
private readonly KEY_PREFIX = 'mcp:session:';
|
|
16
|
-
private readonly CREDENTIALS_KEY_PREFIX = 'mcp:credentials:';
|
|
17
16
|
private readonly USER_ID_KEY_PREFIX = 'mcp:userId:';
|
|
18
17
|
private readonly USER_ID_KEY_SUFFIX = ':sessions';
|
|
19
18
|
|
|
@@ -36,10 +35,6 @@ export class RedisStorageBackend implements SessionStore {
|
|
|
36
35
|
return `${this.KEY_PREFIX}${userId}:${sessionId}`;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
private getCredentialsKey(userId: string, sessionId: string): string {
|
|
40
|
-
return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
38
|
/**
|
|
44
39
|
* Generates Redis key for tracking all sessions for a user
|
|
45
40
|
* @private
|
|
@@ -150,38 +145,29 @@ export class RedisStorageBackend implements SessionStore {
|
|
|
150
145
|
|
|
151
146
|
async patchCredentials(userId: string, sessionId: string, data: Partial<SessionCredentials>): Promise<void> {
|
|
152
147
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
if (!sessionExists) {
|
|
148
|
+
const currentStr = await this.redis.get(sessionKey);
|
|
149
|
+
if (!currentStr) {
|
|
156
150
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
157
151
|
}
|
|
158
152
|
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
const effectiveTtl =
|
|
162
|
-
|
|
163
|
-
: resolveSessionRedisTtlSeconds(session ?? { status: 'pending' });
|
|
164
|
-
const currentStr = await this.redis.get(credentialsKey);
|
|
165
|
-
const current = currentStr ? JSON.parse(currentStr) as SessionCredentials : { sessionId, userId };
|
|
166
|
-
const credentials = { ...current, ...data, sessionId, userId };
|
|
167
|
-
if (effectiveTtl > 0) {
|
|
168
|
-
await this.redis.set(credentialsKey, JSON.stringify(credentials), 'EX', effectiveTtl);
|
|
169
|
-
} else {
|
|
170
|
-
await this.redis.set(credentialsKey, JSON.stringify(credentials));
|
|
171
|
-
}
|
|
153
|
+
const current = JSON.parse(currentStr) as Session;
|
|
154
|
+
const updated = { ...current, ...data, sessionId, userId };
|
|
155
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
|
|
156
|
+
await this.redis.set(sessionKey, JSON.stringify(updated), 'EX', effectiveTtl);
|
|
172
157
|
}
|
|
173
158
|
|
|
174
|
-
async get(userId: string, sessionId: string): Promise<
|
|
159
|
+
async get(userId: string, sessionId: string, options?: GetOptions): Promise<SessionResult | null> {
|
|
175
160
|
try {
|
|
176
161
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
177
162
|
const sessionDataStr = await this.redis.get(sessionKey);
|
|
163
|
+
if (!sessionDataStr) return null;
|
|
178
164
|
|
|
179
|
-
|
|
180
|
-
|
|
165
|
+
const session = normalizeStoredSession(JSON.parse(sessionDataStr) as Session);
|
|
166
|
+
if (!options?.includeCredentials) {
|
|
167
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
168
|
+
return sessionOnly as Session;
|
|
181
169
|
}
|
|
182
|
-
|
|
183
|
-
const session: Session = JSON.parse(sessionDataStr);
|
|
184
|
-
return normalizeStoredSession(session);
|
|
170
|
+
return session;
|
|
185
171
|
} catch (error) {
|
|
186
172
|
console.error('[RedisStorageBackend] Failed to get session:', error);
|
|
187
173
|
return null;
|
|
@@ -189,11 +175,16 @@ export class RedisStorageBackend implements SessionStore {
|
|
|
189
175
|
}
|
|
190
176
|
|
|
191
177
|
async getCredentials(userId: string, sessionId: string): Promise<SessionCredentials | null> {
|
|
192
|
-
const session = await this.get(userId, sessionId);
|
|
178
|
+
const session = await this.get(userId, sessionId, { includeCredentials: true });
|
|
193
179
|
if (!session) return null;
|
|
194
180
|
|
|
195
|
-
const
|
|
196
|
-
return
|
|
181
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
182
|
+
return {
|
|
183
|
+
sessionId, userId,
|
|
184
|
+
clientInformation, tokens, codeVerifier,
|
|
185
|
+
codeVerifierChallenge, codeVerifierNonce,
|
|
186
|
+
clientId, oauthState,
|
|
187
|
+
};
|
|
197
188
|
}
|
|
198
189
|
|
|
199
190
|
async clearCredentials(userId: string, sessionId: string): Promise<void> {
|
|
@@ -201,6 +192,8 @@ export class RedisStorageBackend implements SessionStore {
|
|
|
201
192
|
clientInformation: null,
|
|
202
193
|
tokens: null,
|
|
203
194
|
codeVerifier: null,
|
|
195
|
+
codeVerifierChallenge: null,
|
|
196
|
+
codeVerifierNonce: null,
|
|
204
197
|
clientId: null,
|
|
205
198
|
oauthState: null,
|
|
206
199
|
});
|
|
@@ -241,11 +234,10 @@ export class RedisStorageBackend implements SessionStore {
|
|
|
241
234
|
async delete(userId: string, sessionId: string): Promise<void> {
|
|
242
235
|
try {
|
|
243
236
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
244
|
-
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
245
237
|
const userIdKey = this.getUserIdKey(userId);
|
|
246
238
|
|
|
247
239
|
await this.redis.srem(userIdKey, sessionId);
|
|
248
|
-
await this.redis.del(sessionKey
|
|
240
|
+
await this.redis.del(sessionKey);
|
|
249
241
|
} catch (error) {
|
|
250
242
|
console.error('[RedisStorageBackend] Failed to remove session:', error);
|
|
251
243
|
}
|
|
@@ -280,9 +272,8 @@ export class RedisStorageBackend implements SessionStore {
|
|
|
280
272
|
async clearAll(): Promise<void> {
|
|
281
273
|
try {
|
|
282
274
|
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
283
|
-
const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
|
|
284
275
|
const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
|
|
285
|
-
const allKeys = [...keys, ...
|
|
276
|
+
const allKeys = [...keys, ...userIdKeys];
|
|
286
277
|
if (allKeys.length > 0) {
|
|
287
278
|
await this.redis.del(...allKeys);
|
|
288
279
|
}
|
|
@@ -311,7 +302,6 @@ export class RedisStorageBackend implements SessionStore {
|
|
|
311
302
|
const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
|
|
312
303
|
if (staleSessionIds.length > 0) {
|
|
313
304
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
314
|
-
await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
|
|
315
305
|
}
|
|
316
306
|
|
|
317
307
|
const remainingCount = await this.redis.scard(userIdKey);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Database } from 'better-sqlite3';
|
|
2
|
-
import type { SessionStore, Session, SessionCredentials } from './types.js';
|
|
2
|
+
import type { SessionStore, Session, SessionCredentials, GetOptions, SessionResult } from './types.js';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import { generateSessionId } from '../../shared/utils.js';
|
|
@@ -18,14 +18,12 @@ export interface SqliteStorageOptions {
|
|
|
18
18
|
export class SqliteStorage implements SessionStore {
|
|
19
19
|
private db: Database | null = null;
|
|
20
20
|
private table: string;
|
|
21
|
-
private credentialsTable: string;
|
|
22
21
|
private initialized = false;
|
|
23
22
|
private dbPath: string;
|
|
24
23
|
|
|
25
24
|
constructor(options: SqliteStorageOptions = {}) {
|
|
26
25
|
this.dbPath = options.path || './sessions.db';
|
|
27
26
|
this.table = options.table || 'mcp_sessions';
|
|
28
|
-
this.credentialsTable = `${this.table}_credentials`;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
async init(): Promise<void> {
|
|
@@ -51,13 +49,6 @@ export class SqliteStorage implements SessionStore {
|
|
|
51
49
|
expiresAt INTEGER
|
|
52
50
|
);
|
|
53
51
|
CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
|
|
54
|
-
CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
|
|
55
|
-
sessionId TEXT NOT NULL,
|
|
56
|
-
userId TEXT NOT NULL,
|
|
57
|
-
data TEXT NOT NULL,
|
|
58
|
-
PRIMARY KEY (userId, sessionId),
|
|
59
|
-
FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
|
|
60
|
-
);
|
|
61
52
|
`);
|
|
62
53
|
|
|
63
54
|
this.initialized = true;
|
|
@@ -127,22 +118,19 @@ export class SqliteStorage implements SessionStore {
|
|
|
127
118
|
|
|
128
119
|
async patchCredentials(userId: string, sessionId: string, data: Partial<SessionCredentials>): Promise<void> {
|
|
129
120
|
this.ensureInitialized();
|
|
130
|
-
|
|
121
|
+
const currentSession = await this.get(userId, sessionId, { includeCredentials: true });
|
|
122
|
+
if (!currentSession) {
|
|
131
123
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
132
124
|
}
|
|
133
125
|
|
|
134
|
-
const
|
|
135
|
-
const credentials = { ...current, ...data, sessionId, userId };
|
|
126
|
+
const updated = { ...currentSession, ...data, sessionId, userId };
|
|
136
127
|
const stmt = this.db!.prepare(
|
|
137
|
-
`
|
|
138
|
-
VALUES (?, ?, ?)
|
|
139
|
-
ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
|
|
128
|
+
`UPDATE ${this.table} SET data = ? WHERE sessionId = ? AND userId = ?`
|
|
140
129
|
);
|
|
141
|
-
stmt.run(
|
|
142
|
-
|
|
130
|
+
stmt.run(JSON.stringify(updated), sessionId, userId);
|
|
143
131
|
}
|
|
144
132
|
|
|
145
|
-
async get(userId: string, sessionId: string): Promise<
|
|
133
|
+
async get(userId: string, sessionId: string, options?: GetOptions): Promise<SessionResult | null> {
|
|
146
134
|
this.ensureInitialized();
|
|
147
135
|
|
|
148
136
|
const stmt = this.db!.prepare(
|
|
@@ -151,18 +139,27 @@ export class SqliteStorage implements SessionStore {
|
|
|
151
139
|
const row = stmt.get(sessionId, userId) as { data: string } | undefined;
|
|
152
140
|
|
|
153
141
|
if (!row) return null;
|
|
154
|
-
|
|
142
|
+
|
|
143
|
+
const session = normalizeStoredSession(JSON.parse(row.data) as Session);
|
|
144
|
+
if (!options?.includeCredentials) {
|
|
145
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
146
|
+
return sessionOnly as Session;
|
|
147
|
+
}
|
|
148
|
+
return session;
|
|
155
149
|
}
|
|
156
150
|
|
|
157
151
|
async getCredentials(userId: string, sessionId: string): Promise<SessionCredentials | null> {
|
|
158
152
|
this.ensureInitialized();
|
|
159
|
-
|
|
153
|
+
const session = await this.get(userId, sessionId, { includeCredentials: true });
|
|
154
|
+
if (!session) return null;
|
|
160
155
|
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
156
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
157
|
+
return {
|
|
158
|
+
sessionId, userId,
|
|
159
|
+
clientInformation, tokens, codeVerifier,
|
|
160
|
+
codeVerifierChallenge, codeVerifierNonce,
|
|
161
|
+
clientId, oauthState,
|
|
162
|
+
};
|
|
166
163
|
}
|
|
167
164
|
|
|
168
165
|
async clearCredentials(userId: string, sessionId: string): Promise<void> {
|
|
@@ -170,6 +167,8 @@ export class SqliteStorage implements SessionStore {
|
|
|
170
167
|
clientInformation: null,
|
|
171
168
|
tokens: null,
|
|
172
169
|
codeVerifier: null,
|
|
170
|
+
codeVerifierChallenge: null,
|
|
171
|
+
codeVerifierNonce: null,
|
|
173
172
|
clientId: null,
|
|
174
173
|
oauthState: null,
|
|
175
174
|
});
|
|
@@ -202,9 +201,6 @@ export class SqliteStorage implements SessionStore {
|
|
|
202
201
|
const stmt = this.db!.prepare(
|
|
203
202
|
`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
204
203
|
);
|
|
205
|
-
this.db!.prepare(
|
|
206
|
-
`DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
207
|
-
).run(sessionId, userId);
|
|
208
204
|
stmt.run(sessionId, userId);
|
|
209
205
|
}
|
|
210
206
|
|
|
@@ -217,7 +213,6 @@ export class SqliteStorage implements SessionStore {
|
|
|
217
213
|
|
|
218
214
|
async clearAll(): Promise<void> {
|
|
219
215
|
this.ensureInitialized();
|
|
220
|
-
this.db!.prepare(`DELETE FROM ${this.credentialsTable}`).run();
|
|
221
216
|
const stmt = this.db!.prepare(`DELETE FROM ${this.table}`);
|
|
222
217
|
stmt.run();
|
|
223
218
|
}
|
|
@@ -237,15 +232,6 @@ export class SqliteStorage implements SessionStore {
|
|
|
237
232
|
deleteStmt.run(row.sessionId, row.userId);
|
|
238
233
|
}
|
|
239
234
|
}
|
|
240
|
-
|
|
241
|
-
this.db!.prepare(
|
|
242
|
-
`DELETE FROM ${this.credentialsTable}
|
|
243
|
-
WHERE NOT EXISTS (
|
|
244
|
-
SELECT 1 FROM ${this.table}
|
|
245
|
-
WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
|
|
246
|
-
AND ${this.table}.userId = ${this.credentialsTable}.userId
|
|
247
|
-
)`
|
|
248
|
-
).run();
|
|
249
235
|
}
|
|
250
236
|
|
|
251
237
|
async disconnect(): Promise<void> {
|