@mcp-ts/sdk 1.3.3 → 1.3.5

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 (58) hide show
  1. package/README.md +404 -405
  2. package/dist/adapters/agui-adapter.d.mts +1 -1
  3. package/dist/adapters/agui-adapter.d.ts +1 -1
  4. package/dist/adapters/agui-middleware.d.mts +1 -1
  5. package/dist/adapters/agui-middleware.d.ts +1 -1
  6. package/dist/adapters/ai-adapter.d.mts +1 -1
  7. package/dist/adapters/ai-adapter.d.ts +1 -1
  8. package/dist/adapters/langchain-adapter.d.mts +1 -1
  9. package/dist/adapters/langchain-adapter.d.ts +1 -1
  10. package/dist/adapters/mastra-adapter.d.mts +1 -1
  11. package/dist/adapters/mastra-adapter.d.ts +1 -1
  12. package/dist/client/index.d.mts +1 -0
  13. package/dist/client/index.d.ts +1 -0
  14. package/dist/client/index.js +14 -5
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/index.mjs +14 -5
  17. package/dist/client/index.mjs.map +1 -1
  18. package/dist/client/react.js +15 -6
  19. package/dist/client/react.js.map +1 -1
  20. package/dist/client/react.mjs +15 -6
  21. package/dist/client/react.mjs.map +1 -1
  22. package/dist/client/vue.js +15 -6
  23. package/dist/client/vue.js.map +1 -1
  24. package/dist/client/vue.mjs +15 -6
  25. package/dist/client/vue.mjs.map +1 -1
  26. package/dist/index.d.mts +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +201 -158
  29. package/dist/index.js.map +1 -1
  30. package/dist/index.mjs +201 -158
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/{multi-session-client-FAFpUzZ4.d.ts → multi-session-client-BYLarghq.d.ts} +29 -19
  33. package/dist/{multi-session-client-DzjmT7FX.d.mts → multi-session-client-CzhMkE0k.d.mts} +29 -19
  34. package/dist/server/index.d.mts +1 -1
  35. package/dist/server/index.d.ts +1 -1
  36. package/dist/server/index.js +193 -151
  37. package/dist/server/index.js.map +1 -1
  38. package/dist/server/index.mjs +193 -151
  39. package/dist/server/index.mjs.map +1 -1
  40. package/dist/shared/index.d.mts +2 -2
  41. package/dist/shared/index.d.ts +2 -2
  42. package/dist/shared/index.js +2 -2
  43. package/dist/shared/index.js.map +1 -1
  44. package/dist/shared/index.mjs +2 -2
  45. package/dist/shared/index.mjs.map +1 -1
  46. package/package.json +1 -1
  47. package/src/client/core/sse-client.ts +371 -354
  48. package/src/client/react/use-mcp.ts +31 -31
  49. package/src/client/vue/use-mcp.ts +77 -77
  50. package/src/server/handlers/nextjs-handler.ts +194 -197
  51. package/src/server/handlers/sse-handler.ts +62 -111
  52. package/src/server/mcp/oauth-client.ts +67 -79
  53. package/src/server/mcp/storage-oauth-provider.ts +71 -38
  54. package/src/server/storage/index.ts +15 -13
  55. package/src/server/storage/redis-backend.ts +93 -23
  56. package/src/server/storage/types.ts +12 -12
  57. package/src/shared/constants.ts +2 -2
  58. package/src/shared/event-routing.ts +28 -0
@@ -28,15 +28,18 @@ import {
28
28
  ReadResourceResult,
29
29
  ReadResourceResultSchema,
30
30
  } from '@modelcontextprotocol/sdk/types.js';
31
- import type { OAuthClientMetadata, OAuthTokens, OAuthClientInformationFull } from '@modelcontextprotocol/sdk/shared/auth.js';
31
+ import type { OAuthTokens, OAuthClientInformationFull } from '@modelcontextprotocol/sdk/shared/auth.js';
32
32
  import { StorageOAuthClientProvider, type AgentsOAuthProvider } from './storage-oauth-provider.js';
33
33
  import { sanitizeServerLabel } from '../../shared/utils.js';
34
34
  import { Emitter, type McpConnectionEvent, type McpObservabilityEvent, type McpConnectionState } from '../../shared/events.js';
35
35
  import { UnauthorizedError } from '../../shared/errors.js';
36
36
  import { storage } from '../storage/index.js';
37
- import { SESSION_TTL_SECONDS, STATE_EXPIRATION_MS } from '../../shared/constants.js';
38
-
39
-
37
+ import {
38
+ MCP_CLIENT_NAME,
39
+ MCP_CLIENT_VERSION,
40
+ SESSION_TTL_SECONDS,
41
+ STATE_EXPIRATION_MS,
42
+ } from '../../shared/constants.js';
40
43
 
41
44
  /**
42
45
  * Supported MCP transport types
@@ -300,49 +303,34 @@ export class MCPClient {
300
303
  throw new Error('Missing required connection metadata');
301
304
  }
302
305
 
303
- const clientMetadata: OAuthClientMetadata = {
304
- client_name: this.clientName || 'MCP Assistant',
305
- redirect_uris: [this.callbackUrl],
306
- grant_types: ['authorization_code', 'refresh_token'],
307
- response_types: ['code'],
308
- token_endpoint_auth_method: this.clientSecret ? 'client_secret_basic' : 'none',
309
- client_uri: this.clientUri || 'https://mcp-assistant.in',
310
- logo_uri: this.logoUri || 'https://mcp-assistant.in/logo.png',
311
- policy_uri: this.policyUri || 'https://mcp-assistant.in/privacy',
312
- software_id: '@mcp-ts',
313
- software_version: '1.0.0-beta.4',
314
- ...(this.clientId ? { client_id: this.clientId } : {}),
315
- ...(this.clientSecret ? { client_secret: this.clientSecret } : {}),
316
- };
317
-
318
306
  if (!this.oauthProvider) {
319
307
  if (!this.serverId) {
320
308
  throw new Error('serverId required for OAuth provider initialization');
321
309
  }
322
-
323
- this.oauthProvider = new StorageOAuthClientProvider(
324
- this.identity,
325
- this.serverId,
326
- this.sessionId,
327
- clientMetadata.client_name ?? 'MCP Assistant',
328
- this.callbackUrl,
329
- (redirectUrl: string) => {
310
+ this.oauthProvider = new StorageOAuthClientProvider({
311
+ identity: this.identity,
312
+ serverId: this.serverId,
313
+ sessionId: this.sessionId,
314
+ redirectUrl: this.callbackUrl!,
315
+ clientName: this.clientName,
316
+ clientUri: this.clientUri,
317
+ logoUri: this.logoUri,
318
+ policyUri: this.policyUri,
319
+ clientId: this.clientId,
320
+ clientSecret: this.clientSecret,
321
+ onRedirect: (redirectUrl: string) => {
330
322
  if (this.onRedirect) {
331
323
  this.onRedirect(redirectUrl);
332
324
  }
333
325
  }
334
- );
335
-
336
- if (this.clientId && this.oauthProvider) {
337
- this.oauthProvider.clientId = this.clientId;
338
- }
326
+ });
339
327
  }
340
328
 
341
329
  if (!this.client) {
342
330
  this.client = new Client(
343
331
  {
344
- name: 'mcp-ts-oauth-client',
345
- version: '2.0',
332
+ name: MCP_CLIENT_NAME,
333
+ version: MCP_CLIENT_VERSION,
346
334
  },
347
335
  {
348
336
  capabilities: {
@@ -363,34 +351,34 @@ export class MCPClient {
363
351
  if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
364
352
  this.createdAt = Date.now();
365
353
  console.log(`[MCPClient] Creating initial session ${this.sessionId} for OAuth flow`);
366
- await storage.createSession({
367
- sessionId: this.sessionId,
368
- identity: this.identity,
369
- serverId: this.serverId,
370
- serverName: this.serverName,
371
- serverUrl: this.serverUrl,
372
- callbackUrl: this.callbackUrl,
373
- transportType: this.transportType || 'streamable_http',
374
- createdAt: this.createdAt,
375
- active: false,
376
- }, Math.floor(STATE_EXPIRATION_MS / 1000)); // Short TTL until connection succeeds
377
- }
378
- }
354
+ await storage.createSession({
355
+ sessionId: this.sessionId,
356
+ identity: this.identity,
357
+ serverId: this.serverId,
358
+ serverName: this.serverName,
359
+ serverUrl: this.serverUrl,
360
+ callbackUrl: this.callbackUrl,
361
+ transportType: this.transportType || 'streamable_http',
362
+ createdAt: this.createdAt,
363
+ active: false,
364
+ }, Math.floor(STATE_EXPIRATION_MS / 1000)); // Short TTL until connection succeeds
365
+ }
366
+ }
379
367
 
380
368
  /**
381
369
  * Saves current session state to storage
382
370
  * Creates new session if it doesn't exist, updates if it does
383
- * @param ttl - Time-to-live in seconds (defaults to 12hr for connected sessions)
384
- * @param active - Session status marker used to avoid unnecessary TTL rewrites
385
- * @private
386
- */
387
- private async saveSession(
388
- ttl: number = SESSION_TTL_SECONDS,
389
- active: boolean = true
390
- ): Promise<void> {
391
- if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
392
- return;
393
- }
371
+ * @param ttl - Time-to-live in seconds (defaults to 12hr for connected sessions)
372
+ * @param active - Session status marker used to avoid unnecessary TTL rewrites
373
+ * @private
374
+ */
375
+ private async saveSession(
376
+ ttl: number = SESSION_TTL_SECONDS,
377
+ active: boolean = true
378
+ ): Promise<void> {
379
+ if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
380
+ return;
381
+ }
394
382
 
395
383
  const sessionData = {
396
384
  sessionId: this.sessionId,
@@ -398,11 +386,11 @@ export class MCPClient {
398
386
  serverId: this.serverId,
399
387
  serverName: this.serverName,
400
388
  serverUrl: this.serverUrl,
401
- callbackUrl: this.callbackUrl,
402
- transportType: (this.transportType || 'streamable_http') as TransportType,
403
- createdAt: this.createdAt || Date.now(),
404
- active,
405
- };
389
+ callbackUrl: this.callbackUrl,
390
+ transportType: (this.transportType || 'streamable_http') as TransportType,
391
+ createdAt: this.createdAt || Date.now(),
392
+ active,
393
+ };
406
394
 
407
395
  // Try to update first, create if doesn't exist
408
396
  const existingSession = await storage.getSession(this.identity, this.sessionId);
@@ -512,16 +500,16 @@ export class MCPClient {
512
500
  this.emitStateChange('CONNECTED');
513
501
  this.emitProgress('Connected successfully');
514
502
 
515
- // Promote short-lived OAuth-pending session TTL to long-lived active TTL once.
516
- // Also persist when transport negotiation changed the effective transport.
517
- const existingSession = await storage.getSession(this.identity, this.sessionId);
518
- const needsTransportUpdate = !existingSession || existingSession.transportType !== this.transportType;
519
- const needsTtlPromotion = !existingSession || existingSession.active !== true;
520
-
521
- if (needsTransportUpdate || needsTtlPromotion) {
522
- console.log(`[MCPClient] Saving session ${this.sessionId} with 12hr TTL (connect success)`);
523
- await this.saveSession(SESSION_TTL_SECONDS, true);
524
- }
503
+ // Promote short-lived OAuth-pending session TTL to long-lived active TTL once.
504
+ // Also persist when transport negotiation changed the effective transport.
505
+ const existingSession = await storage.getSession(this.identity, this.sessionId);
506
+ const needsTransportUpdate = !existingSession || existingSession.transportType !== this.transportType;
507
+ const needsTtlPromotion = !existingSession || existingSession.active !== true;
508
+
509
+ if (needsTransportUpdate || needsTtlPromotion) {
510
+ console.log(`[MCPClient] Saving session ${this.sessionId} with 12hr TTL (connect success)`);
511
+ await this.saveSession(SESSION_TTL_SECONDS, true);
512
+ }
525
513
  } catch (error) {
526
514
  /** Handle Authentication Errors */
527
515
  if (
@@ -531,7 +519,7 @@ export class MCPClient {
531
519
  this.emitStateChange('AUTHENTICATING');
532
520
  // Save session with 10min TTL for OAuth pending state
533
521
  console.log(`[MCPClient] Saving session ${this.sessionId} with 10min TTL (OAuth pending)`);
534
- await this.saveSession(Math.floor(STATE_EXPIRATION_MS / 1000), false);
522
+ await this.saveSession(Math.floor(STATE_EXPIRATION_MS / 1000), false);
535
523
 
536
524
  /** Get OAuth authorization URL if available */
537
525
  let authUrl = '';
@@ -620,8 +608,8 @@ export class MCPClient {
620
608
 
621
609
  this.client = new Client(
622
610
  {
623
- name: 'mcp-ts-oauth-client',
624
- version: '2.0',
611
+ name: MCP_CLIENT_NAME,
612
+ version: MCP_CLIENT_VERSION,
625
613
  },
626
614
  {
627
615
  capabilities: {
@@ -642,7 +630,7 @@ export class MCPClient {
642
630
  this.emitStateChange('CONNECTED');
643
631
  // Update session with 12hr TTL after successful OAuth
644
632
  console.log(`[MCPClient] Updating session ${this.sessionId} to 12hr TTL (OAuth complete)`);
645
- await this.saveSession(SESSION_TTL_SECONDS, true);
633
+ await this.saveSession(SESSION_TTL_SECONDS, true);
646
634
 
647
635
  return; // Success, exit function
648
636
 
@@ -980,8 +968,8 @@ export class MCPClient {
980
968
 
981
969
  this.client = new Client(
982
970
  {
983
- name: 'mcp-ts-oauth-client',
984
- version: '2.0',
971
+ name: MCP_CLIENT_NAME,
972
+ version: MCP_CLIENT_VERSION,
985
973
  },
986
974
  { capabilities: {} }
987
975
  );
@@ -1,13 +1,20 @@
1
-
2
1
  import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
3
2
  import type {
4
- OAuthClientInformation,
5
3
  OAuthClientInformationFull,
4
+ OAuthClientInformationMixed,
6
5
  OAuthClientMetadata,
7
6
  OAuthTokens
8
7
  } from "@modelcontextprotocol/sdk/shared/auth.js";
9
8
  import { storage, SessionData } from "../storage/index.js";
10
- import { TOKEN_EXPIRY_BUFFER_MS } from '../../shared/constants.js';
9
+ import {
10
+ DEFAULT_CLIENT_NAME,
11
+ DEFAULT_CLIENT_URI,
12
+ DEFAULT_LOGO_URI,
13
+ DEFAULT_POLICY_URI,
14
+ SOFTWARE_ID,
15
+ SOFTWARE_VERSION,
16
+ TOKEN_EXPIRY_BUFFER_MS,
17
+ } from '../../shared/constants.js';
11
18
 
12
19
  /**
13
20
  * Extension of OAuthClientProvider interface with additional methods
@@ -26,56 +33,74 @@ export interface AgentsOAuthProvider extends OAuthClientProvider {
26
33
  setTokenExpiresAt(expiresAt: number): void;
27
34
  }
28
35
 
36
+ export interface StorageOAuthClientProviderOptions {
37
+ identity: string;
38
+ serverId: string;
39
+ sessionId: string;
40
+ redirectUrl: string;
41
+ clientName?: string;
42
+ clientUri?: string;
43
+ logoUri?: string;
44
+ policyUri?: string;
45
+ clientId?: string;
46
+ clientSecret?: string;
47
+ onRedirect?: (url: string) => void;
48
+ }
49
+
29
50
  /**
30
51
  * Storage-backed OAuth provider implementation for MCP
31
52
  * Stores OAuth tokens, client information, and PKCE verifiers using the configured StorageBackend
32
53
  */
33
54
  export class StorageOAuthClientProvider implements AgentsOAuthProvider {
55
+ public readonly identity: string;
56
+ public readonly serverId: string;
57
+ public readonly sessionId: string;
58
+ public readonly redirectUrl: string;
59
+
60
+ private readonly clientName?: string;
61
+ private readonly clientUri?: string;
62
+ private readonly logoUri?: string;
63
+ private readonly policyUri?: string;
64
+ private readonly clientSecret?: string;
65
+
34
66
  private _authUrl: string | undefined;
35
67
  private _clientId: string | undefined;
36
68
  private onRedirectCallback?: (url: string) => void;
37
69
  private tokenExpiresAt?: number;
38
70
 
39
71
  /**
40
- * Creates a new Storage-backed OAuth provider
41
- * @param identity - User/Client identifier
42
- * @param serverId - Server identifier (for tracking which server this OAuth session belongs to)
43
- * @param sessionId - Session identifier (used as OAuth state)
44
- * @param clientName - OAuth client name
45
- * @param baseRedirectUrl - OAuth callback URL
46
- * @param onRedirect - Optional callback when redirect to authorization is needed
72
+ * Creates a new storage-backed OAuth provider
73
+ * @param options - Provider configuration
47
74
  */
48
- constructor(
49
- public identity: string,
50
- public serverId: string,
51
- public sessionId: string,
52
- public clientName: string,
53
- public baseRedirectUrl: string,
54
- onRedirect?: (url: string) => void
55
- ) {
56
- this.onRedirectCallback = onRedirect;
75
+ constructor(options: StorageOAuthClientProviderOptions) {
76
+ this.identity = options.identity;
77
+ this.serverId = options.serverId;
78
+ this.sessionId = options.sessionId;
79
+ this.redirectUrl = options.redirectUrl;
80
+ this.clientName = options.clientName;
81
+ this.clientUri = options.clientUri;
82
+ this.logoUri = options.logoUri;
83
+ this.policyUri = options.policyUri;
84
+ this._clientId = options.clientId;
85
+ this.clientSecret = options.clientSecret;
86
+ this.onRedirectCallback = options.onRedirect;
57
87
  }
58
88
 
59
89
  get clientMetadata(): OAuthClientMetadata {
60
90
  return {
61
- client_name: this.clientName,
62
- client_uri: this.clientUri,
91
+ client_name: this.clientName || DEFAULT_CLIENT_NAME,
92
+ client_uri: this.clientUri || DEFAULT_CLIENT_URI,
93
+ logo_uri: this.logoUri || DEFAULT_LOGO_URI,
94
+ policy_uri: this.policyUri || DEFAULT_POLICY_URI,
63
95
  grant_types: ["authorization_code", "refresh_token"],
64
96
  redirect_uris: [this.redirectUrl],
65
97
  response_types: ["code"],
66
- token_endpoint_auth_method: "none",
67
- ...(this._clientId ? { client_id: this._clientId } : {})
98
+ token_endpoint_auth_method: this.clientSecret ? "client_secret_basic" : "none",
99
+ software_id: SOFTWARE_ID,
100
+ software_version: SOFTWARE_VERSION,
68
101
  };
69
102
  }
70
103
 
71
- get clientUri() {
72
- return new URL(this.redirectUrl).origin;
73
- }
74
-
75
- get redirectUrl() {
76
- return this.baseRedirectUrl;
77
- }
78
-
79
104
  get clientId() {
80
105
  return this._clientId;
81
106
  }
@@ -91,7 +116,6 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
91
116
  private async getSessionData(): Promise<SessionData> {
92
117
  const data = await storage.getSession(this.identity, this.sessionId);
93
118
  if (!data) {
94
- // Return empty/partial object if not found
95
119
  return {} as SessionData;
96
120
  }
97
121
  return data;
@@ -110,14 +134,25 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
110
134
  /**
111
135
  * Retrieves stored OAuth client information
112
136
  */
113
- async clientInformation(): Promise<OAuthClientInformation | undefined> {
137
+ async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {
114
138
  const data = await this.getSessionData();
115
139
 
116
140
  if (data.clientId && !this._clientId) {
117
141
  this._clientId = data.clientId;
118
142
  }
119
143
 
120
- return data.clientInformation;
144
+ if (data.clientInformation) {
145
+ return data.clientInformation;
146
+ }
147
+
148
+ if (!this._clientId) {
149
+ return undefined;
150
+ }
151
+
152
+ return {
153
+ client_id: this._clientId,
154
+ ...(this.clientSecret ? { client_secret: this.clientSecret } : {}),
155
+ };
121
156
  }
122
157
 
123
158
  /**
@@ -152,7 +187,7 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
152
187
  return this.sessionId;
153
188
  }
154
189
 
155
- async checkState(state: string): Promise<{ valid: boolean; serverId?: string; error?: string }> {
190
+ async checkState(_state: string): Promise<{ valid: boolean; serverId?: string; error?: string }> {
156
191
  const data = await storage.getSession(this.identity, this.sessionId);
157
192
 
158
193
  if (!data) {
@@ -162,7 +197,7 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
162
197
  return { valid: true, serverId: this.serverId };
163
198
  }
164
199
 
165
- async consumeState(state: string): Promise<void> {
200
+ async consumeState(_state: string): Promise<void> {
166
201
  // No-op
167
202
  }
168
203
 
@@ -179,8 +214,6 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
179
214
  if (scope === "all") {
180
215
  await storage.removeSession(this.identity, this.sessionId);
181
216
  } else {
182
- const data = await this.getSessionData();
183
- // Create a copy to modify
184
217
  const updates: Partial<SessionData> = {};
185
218
 
186
219
  if (scope === "client") {
@@ -12,6 +12,13 @@ export { RedisStorageBackend, MemoryStorageBackend, FileStorageBackend, SqliteSt
12
12
  let storageInstance: StorageBackend | null = null;
13
13
  let storagePromise: Promise<StorageBackend> | null = null;
14
14
 
15
+ async function initializeStorage<T extends StorageBackend>(store: T): Promise<T> {
16
+ if (typeof store.init === 'function') {
17
+ await store.init();
18
+ }
19
+ return store;
20
+ }
21
+
15
22
  async function createStorage(): Promise<StorageBackend> {
16
23
  const type = process.env.MCP_TS_STORAGE_TYPE?.toLowerCase();
17
24
 
@@ -38,17 +45,13 @@ async function createStorage(): Promise<StorageBackend> {
38
45
  console.warn('[Storage] MCP_TS_STORAGE_TYPE is "file" but MCP_TS_STORAGE_FILE is missing');
39
46
  }
40
47
  console.log(`[Storage] Using File storage (${filePath}) (Explicit)`);
41
- const store = new FileStorageBackend({ path: filePath });
42
- store.init().catch(err => console.error('[Storage] Failed to initialize file storage:', err));
43
- return store;
48
+ return await initializeStorage(new FileStorageBackend({ path: filePath }));
44
49
  }
45
50
 
46
51
  if (type === 'sqlite') {
47
52
  const dbPath = process.env.MCP_TS_STORAGE_SQLITE_PATH;
48
53
  console.log(`[Storage] Using SQLite storage (${dbPath || 'default'}) (Explicit)`);
49
- const store = new SqliteStorage({ path: dbPath });
50
- store.init().catch(err => console.error('[Storage] Failed to initialize SQLite storage:', err));
51
- return store;
54
+ return await initializeStorage(new SqliteStorage({ path: dbPath }));
52
55
  }
53
56
 
54
57
  if (type === 'memory') {
@@ -72,16 +75,12 @@ async function createStorage(): Promise<StorageBackend> {
72
75
 
73
76
  if (process.env.MCP_TS_STORAGE_FILE) {
74
77
  console.log(`[Storage] Auto-detected MCP_TS_STORAGE_FILE. Using File storage (${process.env.MCP_TS_STORAGE_FILE}).`);
75
- const store = new FileStorageBackend({ path: process.env.MCP_TS_STORAGE_FILE });
76
- store.init().catch(err => console.error('[Storage] Failed to initialize file storage:', err));
77
- return store;
78
+ return await initializeStorage(new FileStorageBackend({ path: process.env.MCP_TS_STORAGE_FILE }));
78
79
  }
79
80
 
80
81
  if (process.env.MCP_TS_STORAGE_SQLITE_PATH) {
81
82
  console.log(`[Storage] Auto-detected MCP_TS_STORAGE_SQLITE_PATH. Using SQLite storage (${process.env.MCP_TS_STORAGE_SQLITE_PATH}).`);
82
- const store = new SqliteStorage({ path: process.env.MCP_TS_STORAGE_SQLITE_PATH });
83
- store.init().catch(err => console.error('[Storage] Failed to initialize SQLite storage:', err));
84
- return store;
83
+ return await initializeStorage(new SqliteStorage({ path: process.env.MCP_TS_STORAGE_SQLITE_PATH }));
85
84
  }
86
85
 
87
86
  console.log('[Storage] No storage configured. Using In-Memory storage (Default).');
@@ -94,7 +93,10 @@ async function getStorage(): Promise<StorageBackend> {
94
93
  }
95
94
 
96
95
  if (!storagePromise) {
97
- storagePromise = createStorage();
96
+ storagePromise = createStorage().catch((error) => {
97
+ storagePromise = null;
98
+ throw error;
99
+ });
98
100
  }
99
101
 
100
102
  storageInstance = await storagePromise;
@@ -1,7 +1,7 @@
1
1
 
2
2
  import type { Redis } from 'ioredis';
3
3
  import { customAlphabet } from 'nanoid';
4
- import { StorageBackend, SessionData, SetClientOptions } from './types';
4
+ import { StorageBackend, SessionData } from './types';
5
5
  import { SESSION_TTL_SECONDS } from '../../shared/constants.js';
6
6
 
7
7
  /** first char: letters only (required by OpenAI) */
@@ -22,6 +22,8 @@ const rest = customAlphabet(
22
22
  export class RedisStorageBackend implements StorageBackend {
23
23
  private readonly DEFAULT_TTL = SESSION_TTL_SECONDS;
24
24
  private readonly KEY_PREFIX = 'mcp:session:';
25
+ private readonly IDENTITY_KEY_PREFIX = 'mcp:identity:';
26
+ private readonly IDENTITY_KEY_SUFFIX = ':sessions';
25
27
 
26
28
  constructor(private redis: Redis) { }
27
29
 
@@ -38,7 +40,42 @@ export class RedisStorageBackend implements StorageBackend {
38
40
  * @private
39
41
  */
40
42
  private getIdentityKey(identity: string): string {
41
- return `mcp:identity:${identity}:sessions`;
43
+ return `${this.IDENTITY_KEY_PREFIX}${identity}${this.IDENTITY_KEY_SUFFIX}`;
44
+ }
45
+
46
+ private parseIdentityFromKey(identityKey: string): string {
47
+ return identityKey.slice(
48
+ this.IDENTITY_KEY_PREFIX.length,
49
+ identityKey.length - this.IDENTITY_KEY_SUFFIX.length
50
+ );
51
+ }
52
+
53
+ private async scanKeys(pattern: string): Promise<string[]> {
54
+ const redis = this.redis as Redis & {
55
+ scan?: (cursor: string, ...args: Array<string | number>) => Promise<[string, string[]]>;
56
+ };
57
+
58
+ if (typeof redis.scan !== 'function') {
59
+ return await this.redis.keys(pattern);
60
+ }
61
+
62
+ const keys = new Set<string>();
63
+ let cursor = '0';
64
+
65
+ try {
66
+ do {
67
+ const [nextCursor, batch] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
68
+ cursor = nextCursor;
69
+ for (const key of batch) {
70
+ keys.add(key);
71
+ }
72
+ } while (cursor !== '0');
73
+ } catch (error) {
74
+ console.warn('[RedisStorage] SCAN failed, falling back to KEYS:', error);
75
+ return await this.redis.keys(pattern);
76
+ }
77
+
78
+ return Array.from(keys);
42
79
  }
43
80
 
44
81
  generateSessionId(): string {
@@ -121,18 +158,14 @@ export class RedisStorageBackend implements StorageBackend {
121
158
  }
122
159
 
123
160
  async getIdentityMcpSessions(identity: string): Promise<string[]> {
124
- const identityKey = this.getIdentityKey(identity);
125
- try {
126
- return await this.redis.smembers(identityKey);
127
- } catch (error) {
128
- console.error(`[RedisStorage] Failed to get sessions for ${identity}:`, error);
129
- return [];
130
- }
161
+ const sessions = await this.getIdentitySessionsData(identity);
162
+ return sessions.map((session) => session.sessionId);
131
163
  }
132
164
 
133
165
  async getIdentitySessionsData(identity: string): Promise<SessionData[]> {
134
166
  try {
135
- const sessionIds = await this.redis.smembers(this.getIdentityKey(identity));
167
+ const identityKey = this.getIdentityKey(identity);
168
+ const sessionIds = await this.redis.smembers(identityKey);
136
169
  if (sessionIds.length === 0) return [];
137
170
 
138
171
  const results = await Promise.all(
@@ -142,6 +175,11 @@ export class RedisStorageBackend implements StorageBackend {
142
175
  })
143
176
  );
144
177
 
178
+ const staleSessionIds = sessionIds.filter((_, index) => results[index] === null);
179
+ if (staleSessionIds.length > 0) {
180
+ await this.redis.srem(identityKey, ...staleSessionIds);
181
+ }
182
+
145
183
  return results.filter((session): session is SessionData => session !== null);
146
184
  } catch (error) {
147
185
  console.error(`[RedisStorage] Failed to get session data for ${identity}:`, error);
@@ -163,9 +201,24 @@ export class RedisStorageBackend implements StorageBackend {
163
201
 
164
202
  async getAllSessionIds(): Promise<string[]> {
165
203
  try {
166
- const pattern = `${this.KEY_PREFIX}*`;
167
- const keys = await this.redis.keys(pattern);
168
- return keys.map((key) => key.replace(this.KEY_PREFIX, ''));
204
+ const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
205
+ const sessions = await Promise.all(
206
+ keys.map(async (key) => {
207
+ const data = await this.redis.get(key);
208
+ if (!data) {
209
+ return null;
210
+ }
211
+
212
+ try {
213
+ return (JSON.parse(data) as SessionData).sessionId;
214
+ } catch (error) {
215
+ console.error('[RedisStorage] Failed to parse session while listing all session IDs:', error);
216
+ return null;
217
+ }
218
+ })
219
+ );
220
+
221
+ return sessions.filter((sessionId): sessionId is string => sessionId !== null);
169
222
  } catch (error) {
170
223
  console.error('[RedisStorage] Failed to get all sessions:', error);
171
224
  return [];
@@ -174,10 +227,11 @@ export class RedisStorageBackend implements StorageBackend {
174
227
 
175
228
  async clearAll(): Promise<void> {
176
229
  try {
177
- const pattern = `${this.KEY_PREFIX}*`;
178
- const keys = await this.redis.keys(pattern);
179
- if (keys.length > 0) {
180
- await this.redis.del(...keys);
230
+ const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
231
+ const identityKeys = await this.scanKeys(`${this.IDENTITY_KEY_PREFIX}*${this.IDENTITY_KEY_SUFFIX}`);
232
+ const allKeys = [...keys, ...identityKeys];
233
+ if (allKeys.length > 0) {
234
+ await this.redis.del(...allKeys);
181
235
  }
182
236
  } catch (error) {
183
237
  console.error('[RedisStorage] Failed to clear sessions:', error);
@@ -186,13 +240,29 @@ export class RedisStorageBackend implements StorageBackend {
186
240
 
187
241
  async cleanupExpiredSessions(): Promise<void> {
188
242
  try {
189
- const pattern = `${this.KEY_PREFIX}*`;
190
- const keys = await this.redis.keys(pattern);
243
+ const identityKeys = await this.scanKeys(`${this.IDENTITY_KEY_PREFIX}*${this.IDENTITY_KEY_SUFFIX}`);
244
+
245
+ for (const identityKey of identityKeys) {
246
+ const identity = this.parseIdentityFromKey(identityKey);
247
+ const sessionIds = await this.redis.smembers(identityKey);
248
+
249
+ if (sessionIds.length === 0) {
250
+ await this.redis.del(identityKey);
251
+ continue;
252
+ }
253
+
254
+ const existenceChecks = await Promise.all(
255
+ sessionIds.map((sessionId) => this.redis.exists(this.getSessionKey(identity, sessionId)))
256
+ );
257
+
258
+ const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
259
+ if (staleSessionIds.length > 0) {
260
+ await this.redis.srem(identityKey, ...staleSessionIds);
261
+ }
191
262
 
192
- for (const key of keys) {
193
- const ttl = await this.redis.ttl(key);
194
- if (ttl <= 0) {
195
- await this.redis.del(key);
263
+ const remainingCount = await this.redis.scard(identityKey);
264
+ if (remainingCount === 0) {
265
+ await this.redis.del(identityKey);
196
266
  }
197
267
  }
198
268
  } catch (error) {