@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.
Files changed (73) hide show
  1. package/README.md +3 -574
  2. package/dist/adapters/agui-adapter.d.mts +4 -4
  3. package/dist/adapters/agui-adapter.d.ts +4 -4
  4. package/dist/adapters/agui-middleware.d.mts +4 -4
  5. package/dist/adapters/agui-middleware.d.ts +4 -4
  6. package/dist/adapters/ai-adapter.d.mts +4 -4
  7. package/dist/adapters/ai-adapter.d.ts +4 -4
  8. package/dist/adapters/langchain-adapter.d.mts +4 -4
  9. package/dist/adapters/langchain-adapter.d.ts +4 -4
  10. package/dist/adapters/mastra-adapter.d.mts +3 -3
  11. package/dist/adapters/mastra-adapter.d.ts +3 -3
  12. package/dist/client/index.d.mts +2 -2
  13. package/dist/client/index.d.ts +2 -2
  14. package/dist/client/index.js +3 -0
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/index.mjs +3 -0
  17. package/dist/client/index.mjs.map +1 -1
  18. package/dist/client/react.d.mts +10 -18
  19. package/dist/client/react.d.ts +10 -18
  20. package/dist/client/react.js +17 -0
  21. package/dist/client/react.js.map +1 -1
  22. package/dist/client/react.mjs +17 -0
  23. package/dist/client/react.mjs.map +1 -1
  24. package/dist/client/vue.d.mts +4 -4
  25. package/dist/client/vue.d.ts +4 -4
  26. package/dist/client/vue.js +3 -0
  27. package/dist/client/vue.js.map +1 -1
  28. package/dist/client/vue.mjs +3 -0
  29. package/dist/client/vue.mjs.map +1 -1
  30. package/dist/{index-BRZO_JyI.d.mts → index-BrDkbut5.d.ts} +2 -1
  31. package/dist/{index-C3YGagi2.d.ts → index-DHQvlx4K.d.mts} +2 -1
  32. package/dist/index.d.mts +5 -5
  33. package/dist/index.d.ts +5 -5
  34. package/dist/index.js +1056 -876
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +1048 -871
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{multi-session-client-CuacvZaQ.d.ts → multi-session-client-CQzymvHl.d.ts} +183 -227
  39. package/dist/{multi-session-client-DqzT8Oo7.d.mts → multi-session-client-C_Ja2TuV.d.mts} +183 -227
  40. package/dist/server/index.d.mts +338 -88
  41. package/dist/server/index.d.ts +338 -88
  42. package/dist/server/index.js +1053 -876
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +1045 -871
  45. package/dist/server/index.mjs.map +1 -1
  46. package/dist/shared/index.d.mts +4 -4
  47. package/dist/shared/index.d.ts +4 -4
  48. package/dist/shared/index.js.map +1 -1
  49. package/dist/shared/index.mjs.map +1 -1
  50. package/dist/{tool-router-B0qpui-V.d.mts → tool-router-C1n7OXbl.d.mts} +1 -1
  51. package/dist/{tool-router-2qUyZ-cP.d.ts → tool-router-CBIawFnt.d.ts} +1 -1
  52. package/dist/{types-BkJ_rgzo.d.mts → types-DX71u9gR.d.mts} +11 -5
  53. package/dist/{types-BkJ_rgzo.d.ts → types-DX71u9gR.d.ts} +11 -5
  54. package/migrations/neon/20260513010000_install_mcp_sessions.sql +4 -34
  55. package/migrations/supabase/20260330195700_install_mcp_sessions.sql +5 -62
  56. package/package.json +2 -1
  57. package/src/client/core/sse-client.ts +5 -0
  58. package/src/client/react/use-mcp.ts +27 -28
  59. package/src/server/handlers/sse-handler.ts +671 -633
  60. package/src/server/index.ts +2 -1
  61. package/src/server/mcp/multi-session-client.ts +34 -10
  62. package/src/server/mcp/oauth-client.ts +197 -205
  63. package/src/server/mcp/storage-oauth-provider.ts +148 -41
  64. package/src/server/mcp/tool-policy-gateway.ts +11 -1
  65. package/src/server/storage/file-backend.ts +25 -21
  66. package/src/server/storage/index.ts +71 -0
  67. package/src/server/storage/memory-backend.ts +24 -12
  68. package/src/server/storage/neon-backend.ts +70 -72
  69. package/src/server/storage/redis-backend.ts +26 -36
  70. package/src/server/storage/sqlite-backend.ts +25 -39
  71. package/src/server/storage/supabase-backend.ts +45 -54
  72. package/src/server/storage/types.ts +22 -2
  73. 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 SessionCredentials['oauthState'],
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
- await this.sql.query(
249
- `INSERT INTO ${this.credentialsTableName} (
250
- user_id,
251
- session_id,
252
- client_information,
253
- tokens,
254
- code_verifier,
255
- client_id,
256
- oauth_state,
257
- updated_at
258
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, now())
259
- ON CONFLICT (user_id, session_id)
260
- DO UPDATE SET
261
- client_information = CASE WHEN $8 THEN EXCLUDED.client_information ELSE ${this.credentialsTableName}.client_information END,
262
- tokens = CASE WHEN $9 THEN EXCLUDED.tokens ELSE ${this.credentialsTableName}.tokens END,
263
- code_verifier = CASE WHEN $10 THEN EXCLUDED.code_verifier ELSE ${this.credentialsTableName}.code_verifier END,
264
- client_id = CASE WHEN $11 THEN EXCLUDED.client_id ELSE ${this.credentialsTableName}.client_id END,
265
- oauth_state = CASE WHEN $12 THEN EXCLUDED.oauth_state ELSE ${this.credentialsTableName}.oauth_state END,
266
- updated_at = now()`,
267
- [
268
- userId,
269
- sessionId,
270
- 'clientInformation' in data ? (data.clientInformation == null ? null : encryptObject(data.clientInformation)) : null,
271
- 'tokens' in data ? (data.tokens == null ? null : encryptObject(data.tokens)) : null,
272
- 'codeVerifier' in data ? (data.codeVerifier == null ? null : encryptObject(data.codeVerifier)) : null,
273
- 'clientId' in data ? (data.clientId ?? null) : null,
274
- 'oauthState' in data ? (data.oauthState ?? null) : null,
275
- 'clientInformation' in data,
276
- 'tokens' in data,
277
- 'codeVerifier' in data,
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<Session | null> {
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 * FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
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 credentialRows = await this.sql.query(
304
- `SELECT * FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
305
- [userId, sessionId]
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 Array<{ id: string }>;
302
+ ) as NeonSessionRow[];
316
303
 
317
- return sessionRows[0] ? { sessionId, userId } : null;
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
- `DELETE FROM ${this.credentialsTableName} WHERE user_id = $1 AND session_id = $2`,
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 credentialsKey = this.getCredentialsKey(userId, sessionId);
154
- const sessionExists = await this.redis.exists(sessionKey);
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 session = await this.get(userId, sessionId);
160
- const currentTtl = await this.redis.ttl(sessionKey);
161
- const effectiveTtl = currentTtl > 0 && session
162
- ? currentTtl
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<Session | null> {
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
- if (!sessionDataStr) {
180
- return null;
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 credentialsStr = await this.redis.get(this.getCredentialsKey(userId, sessionId));
196
- return credentialsStr ? JSON.parse(credentialsStr) as SessionCredentials : { sessionId, userId };
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, credentialsKey);
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, ...credentialKeys, ...userIdKeys];
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
- if (!await this.get(userId, sessionId)) {
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 current = await this.getCredentials(userId, sessionId) ?? { sessionId, userId };
135
- const credentials = { ...current, ...data, sessionId, userId };
126
+ const updated = { ...currentSession, ...data, sessionId, userId };
136
127
  const stmt = this.db!.prepare(
137
- `INSERT INTO ${this.credentialsTable} (sessionId, userId, data)
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(sessionId, userId, JSON.stringify(credentials));
142
-
130
+ stmt.run(JSON.stringify(updated), sessionId, userId);
143
131
  }
144
132
 
145
- async get(userId: string, sessionId: string): Promise<Session | null> {
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
- return normalizeStoredSession(JSON.parse(row.data) as Session);
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
- if (!await this.get(userId, sessionId)) return null;
153
+ const session = await this.get(userId, sessionId, { includeCredentials: true });
154
+ if (!session) return null;
160
155
 
161
- const stmt = this.db!.prepare(
162
- `SELECT data FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
163
- );
164
- const row = stmt.get(sessionId, userId) as { data: string } | undefined;
165
- return row ? JSON.parse(row.data) as SessionCredentials : { sessionId, userId };
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> {