@mcp-ts/sdk 1.6.1 → 2.0.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 (114) hide show
  1. package/README.md +12 -6
  2. package/dist/adapters/agui-adapter.d.mts +3 -3
  3. package/dist/adapters/agui-adapter.d.ts +3 -3
  4. package/dist/adapters/agui-adapter.js +4 -5
  5. package/dist/adapters/agui-adapter.js.map +1 -1
  6. package/dist/adapters/agui-adapter.mjs +4 -5
  7. package/dist/adapters/agui-adapter.mjs.map +1 -1
  8. package/dist/adapters/agui-middleware.d.mts +3 -3
  9. package/dist/adapters/agui-middleware.d.ts +3 -3
  10. package/dist/adapters/ai-adapter.d.mts +9 -3
  11. package/dist/adapters/ai-adapter.d.ts +9 -3
  12. package/dist/adapters/ai-adapter.js +20 -6
  13. package/dist/adapters/ai-adapter.js.map +1 -1
  14. package/dist/adapters/ai-adapter.mjs +20 -6
  15. package/dist/adapters/ai-adapter.mjs.map +1 -1
  16. package/dist/adapters/langchain-adapter.d.mts +3 -3
  17. package/dist/adapters/langchain-adapter.d.ts +3 -3
  18. package/dist/adapters/langchain-adapter.js +9 -6
  19. package/dist/adapters/langchain-adapter.js.map +1 -1
  20. package/dist/adapters/langchain-adapter.mjs +9 -6
  21. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  22. package/dist/adapters/mastra-adapter.d.mts +1 -1
  23. package/dist/adapters/mastra-adapter.d.ts +1 -1
  24. package/dist/adapters/mastra-adapter.js +5 -1
  25. package/dist/adapters/mastra-adapter.js.map +1 -1
  26. package/dist/adapters/mastra-adapter.mjs +5 -1
  27. package/dist/adapters/mastra-adapter.mjs.map +1 -1
  28. package/dist/bin/mcp-ts.js +7 -1
  29. package/dist/bin/mcp-ts.js.map +1 -1
  30. package/dist/bin/mcp-ts.mjs +7 -1
  31. package/dist/bin/mcp-ts.mjs.map +1 -1
  32. package/dist/client/index.d.mts +2 -2
  33. package/dist/client/index.d.ts +2 -2
  34. package/dist/client/index.js +9 -13
  35. package/dist/client/index.js.map +1 -1
  36. package/dist/client/index.mjs +9 -13
  37. package/dist/client/index.mjs.map +1 -1
  38. package/dist/client/react.d.mts +7 -7
  39. package/dist/client/react.d.ts +7 -7
  40. package/dist/client/react.js +111 -63
  41. package/dist/client/react.js.map +1 -1
  42. package/dist/client/react.mjs +111 -63
  43. package/dist/client/react.mjs.map +1 -1
  44. package/dist/client/vue.d.mts +7 -7
  45. package/dist/client/vue.d.ts +7 -7
  46. package/dist/client/vue.js +14 -18
  47. package/dist/client/vue.js.map +1 -1
  48. package/dist/client/vue.mjs +14 -18
  49. package/dist/client/vue.mjs.map +1 -1
  50. package/dist/{index-DhA-OEAe.d.ts → index-C9gvpxy5.d.ts} +5 -5
  51. package/dist/{index-bFL4ZF2N.d.mts → index-eaH14_5u.d.mts} +5 -5
  52. package/dist/index.d.mts +6 -6
  53. package/dist/index.d.ts +6 -6
  54. package/dist/index.js +616 -370
  55. package/dist/index.js.map +1 -1
  56. package/dist/index.mjs +615 -370
  57. package/dist/index.mjs.map +1 -1
  58. package/dist/{multi-session-client-CHE8QpVE.d.ts → multi-session-client-BYtguGJm.d.ts} +22 -22
  59. package/dist/{multi-session-client-CQsRbxYI.d.mts → multi-session-client-DYNe6az3.d.mts} +22 -22
  60. package/dist/server/index.d.mts +31 -34
  61. package/dist/server/index.d.ts +31 -34
  62. package/dist/server/index.js +531 -256
  63. package/dist/server/index.js.map +1 -1
  64. package/dist/server/index.mjs +530 -256
  65. package/dist/server/index.mjs.map +1 -1
  66. package/dist/shared/index.d.mts +5 -5
  67. package/dist/shared/index.d.ts +5 -5
  68. package/dist/shared/index.js +76 -101
  69. package/dist/shared/index.js.map +1 -1
  70. package/dist/shared/index.mjs +76 -101
  71. package/dist/shared/index.mjs.map +1 -1
  72. package/dist/{tool-router-Dh2804tM.d.ts → tool-router-Ddtybmr0.d.ts} +71 -73
  73. package/dist/{tool-router-BVaV1udm.d.mts → tool-router-Dnd6IOKC.d.mts} +71 -73
  74. package/dist/{types-rIuN1CQi.d.mts → types-BCAG20P6.d.mts} +4 -4
  75. package/dist/{types-rIuN1CQi.d.ts → types-BCAG20P6.d.ts} +4 -4
  76. package/dist/{utils-0qmYrqoa.d.mts → utils-DELRKQPU.d.mts} +1 -1
  77. package/dist/{utils-0qmYrqoa.d.ts → utils-DELRKQPU.d.ts} +1 -1
  78. package/migrations/neon/20260513010000_install_mcp_sessions.sql +69 -0
  79. package/migrations/neon/20260513020000_add_session_cleanup_cron.sql +35 -0
  80. package/{supabase/migrations → migrations/supabase}/20260330195700_install_mcp_sessions.sql +7 -9
  81. package/package.json +14 -5
  82. package/src/adapters/ai-adapter.ts +30 -1
  83. package/src/adapters/langchain-adapter.ts +6 -2
  84. package/src/adapters/mastra-adapter.ts +6 -2
  85. package/src/bin/mcp-ts.ts +8 -1
  86. package/src/client/core/app-host.ts +1 -1
  87. package/src/client/core/sse-client.ts +12 -14
  88. package/src/client/core/types.ts +1 -1
  89. package/src/client/react/oauth-popup.tsx +111 -51
  90. package/src/client/react/use-mcp-apps.tsx +1 -1
  91. package/src/client/react/use-mcp.ts +11 -11
  92. package/src/client/vue/use-mcp.ts +10 -10
  93. package/src/server/handlers/nextjs-handler.ts +18 -15
  94. package/src/server/handlers/sse-handler.ts +29 -29
  95. package/src/server/index.ts +1 -1
  96. package/src/server/mcp/multi-session-client.ts +17 -17
  97. package/src/server/mcp/oauth-client.ts +37 -37
  98. package/src/server/mcp/storage-oauth-provider.ts +17 -17
  99. package/src/server/storage/file-backend.ts +25 -25
  100. package/src/server/storage/index.ts +67 -10
  101. package/src/server/storage/memory-backend.ts +34 -34
  102. package/src/server/storage/neon-backend.ts +281 -0
  103. package/src/server/storage/redis-backend.ts +64 -64
  104. package/src/server/storage/sqlite-backend.ts +33 -33
  105. package/src/server/storage/supabase-backend.ts +23 -24
  106. package/src/server/storage/types.ts +18 -21
  107. package/src/shared/errors.ts +1 -1
  108. package/src/shared/index.ts +1 -2
  109. package/src/shared/meta-tools.ts +4 -6
  110. package/src/shared/schema-compressor.ts +2 -42
  111. package/src/shared/tool-index.ts +89 -84
  112. package/src/shared/tool-router.ts +0 -24
  113. package/src/shared/types.ts +4 -4
  114. /package/{supabase/migrations → migrations/supabase}/20260421010000_add_session_cleanup_cron.sql +0 -0
@@ -33,7 +33,7 @@ import { StorageOAuthClientProvider, type AgentsOAuthProvider } from './storage-
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
- import { storage } from '../storage/index.js';
36
+ import { sessions } from '../storage/index.js';
37
37
  import {
38
38
  MCP_CLIENT_NAME,
39
39
  MCP_CLIENT_VERSION,
@@ -44,7 +44,7 @@ import {
44
44
  /**
45
45
  * Supported MCP transport types
46
46
  */
47
- export type TransportType = 'sse' | 'streamable_http';
47
+ export type TransportType = 'sse' | 'streamable-http';
48
48
 
49
49
  /**
50
50
  * Extended capabilities including MCP App support
@@ -65,7 +65,7 @@ export interface MCPOAuthClientOptions {
65
65
  serverName?: string;
66
66
  callbackUrl?: string;
67
67
  onRedirect?: (url: string) => void;
68
- identity: string;
68
+ userId: string;
69
69
  serverId?: string; /** Optional - loaded from session if not provided */
70
70
  sessionId: string; /** Required - primary key for session lookup */
71
71
  transportType?: TransportType;
@@ -88,7 +88,7 @@ export class MCPClient {
88
88
  private client: Client | null = null;
89
89
  public oauthProvider: AgentsOAuthProvider | null = null;
90
90
  private transport: StreamableHTTPClientTransport | SSEClientTransport | null = null;
91
- private identity: string;
91
+ private userId: string;
92
92
  private serverId?: string;
93
93
  private sessionId: string;
94
94
  private serverName?: string;
@@ -118,7 +118,7 @@ export class MCPClient {
118
118
 
119
119
  /**
120
120
  * Creates a new MCP client instance
121
- * Can be initialized with minimal options (identity + sessionId) for session restoration
121
+ * Can be initialized with minimal options (userId + sessionId) for session restoration
122
122
  * @param options - Client configuration options
123
123
  */
124
124
  constructor(options: MCPOAuthClientOptions) {
@@ -126,7 +126,7 @@ export class MCPClient {
126
126
  this.serverName = options.serverName;
127
127
  this.callbackUrl = options.callbackUrl;
128
128
  this.onRedirect = options.onRedirect;
129
- this.identity = options.identity;
129
+ this.userId = options.userId;
130
130
  this.serverId = options.serverId;
131
131
  this.sessionId = options.sessionId;
132
132
  this.transportType = options.transportType;
@@ -283,7 +283,7 @@ export class MCPClient {
283
283
  this.emitProgress('Loading session configuration...');
284
284
 
285
285
  if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
286
- const sessionData = await storage.getSession(this.identity, this.sessionId);
286
+ const sessionData = await sessions.get(this.userId, this.sessionId);
287
287
  if (!sessionData) {
288
288
  throw new Error(`Session not found: ${this.sessionId}`);
289
289
  }
@@ -310,7 +310,7 @@ export class MCPClient {
310
310
  throw new Error('serverId required for OAuth provider initialization');
311
311
  }
312
312
  this.oauthProvider = new StorageOAuthClientProvider({
313
- identity: this.identity,
313
+ userId: this.userId,
314
314
  serverId: this.serverId,
315
315
  sessionId: this.sessionId,
316
316
  redirectUrl: this.callbackUrl!,
@@ -346,21 +346,21 @@ export class MCPClient {
346
346
  );
347
347
  }
348
348
 
349
- // Create session in storage if it doesn't exist yet
349
+ // Create session in the session store if it doesn't exist yet
350
350
  // This is needed BEFORE OAuth flow starts because the OAuth provider
351
351
  // will call saveCodeVerifier() which requires the session to exist
352
- const existingSession = await storage.getSession(this.identity, this.sessionId);
352
+ const existingSession = await sessions.get(this.userId, this.sessionId);
353
353
  if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
354
354
  this.createdAt = Date.now();
355
355
  console.log(`[MCPClient] Creating initial session ${this.sessionId} for OAuth flow`);
356
- await storage.createSession({
356
+ await sessions.create({
357
357
  sessionId: this.sessionId,
358
- identity: this.identity,
358
+ userId: this.userId,
359
359
  serverId: this.serverId,
360
360
  serverName: this.serverName,
361
361
  serverUrl: this.serverUrl,
362
362
  callbackUrl: this.callbackUrl,
363
- transportType: this.transportType || 'streamable_http',
363
+ transportType: this.transportType || 'streamable-http',
364
364
  headers: this.headers,
365
365
  createdAt: this.createdAt,
366
366
  active: false,
@@ -369,7 +369,7 @@ export class MCPClient {
369
369
  }
370
370
 
371
371
  /**
372
- * Saves current session state to storage
372
+ * Saves current session state to the session store
373
373
  * Creates new session if it doesn't exist, updates if it does
374
374
  * @param ttl - Time-to-live in seconds (defaults to 12hr for connected sessions)
375
375
  * @param active - Session status marker used to avoid unnecessary TTL rewrites
@@ -385,23 +385,23 @@ export class MCPClient {
385
385
 
386
386
  const sessionData = {
387
387
  sessionId: this.sessionId,
388
- identity: this.identity,
388
+ userId: this.userId,
389
389
  serverId: this.serverId,
390
390
  serverName: this.serverName,
391
391
  serverUrl: this.serverUrl,
392
392
  callbackUrl: this.callbackUrl,
393
- transportType: (this.transportType || 'streamable_http') as TransportType,
393
+ transportType: (this.transportType || 'streamable-http') as TransportType,
394
394
  headers: this.headers,
395
395
  createdAt: this.createdAt || Date.now(),
396
396
  active,
397
397
  };
398
398
 
399
399
  // Try to update first, create if doesn't exist
400
- const existingSession = await storage.getSession(this.identity, this.sessionId);
400
+ const existingSession = await sessions.get(this.userId, this.sessionId);
401
401
  if (existingSession) {
402
- await storage.updateSession(this.identity, this.sessionId, sessionData, ttl);
402
+ await sessions.update(this.userId, this.sessionId, sessionData, ttl);
403
403
  } else {
404
- await storage.createSession(sessionData, ttl);
404
+ await sessions.create(sessionData, ttl);
405
405
  }
406
406
  }
407
407
 
@@ -417,7 +417,7 @@ export class MCPClient {
417
417
  */
418
418
  const transportsToTry: TransportType[] = this.transportType
419
419
  ? [this.transportType]
420
- : ['streamable_http', 'sse'];
420
+ : ['streamable-http', 'sse'];
421
421
 
422
422
  let lastError: unknown;
423
423
 
@@ -505,7 +505,7 @@ export class MCPClient {
505
505
  this.emitProgress('Connected successfully');
506
506
 
507
507
  // Refresh session metadata on every successful connect so active sessions
508
- // record ongoing usage and don't look dormant to storage cleanup jobs.
508
+ // record ongoing usage and don't look dormant to session cleanup jobs.
509
509
  console.log(`[MCPClient] Saving session ${this.sessionId} with 12hr TTL (connect success)`);
510
510
  await this.saveSession(SESSION_TTL_SECONDS, true);
511
511
  } catch (error) {
@@ -540,7 +540,7 @@ export class MCPClient {
540
540
  // We remove it now to ensure the database remains lean, bypassing the
541
541
  // automated lifecycle sweep.
542
542
  try {
543
- await storage.removeSession(this.identity, this.sessionId);
543
+ await sessions.delete(this.userId, this.sessionId);
544
544
  } catch {
545
545
  // Non-blocking: Proactive cleanup failures are suppressed to prioritize
546
546
  // the original error context.
@@ -579,9 +579,9 @@ export class MCPClient {
579
579
  // Terminal Handshake Failure: only purge transient sessions. Active
580
580
  // sessions may still hold valid credentials for a later reconnect.
581
581
  try {
582
- const existingSession = await storage.getSession(this.identity, this.sessionId);
582
+ const existingSession = await sessions.get(this.userId, this.sessionId);
583
583
  if (!existingSession || existingSession.active !== true) {
584
- await storage.removeSession(this.identity, this.sessionId);
584
+ await sessions.delete(this.userId, this.sessionId);
585
585
  }
586
586
  } catch {
587
587
  // Non-blocking: Cleanup is performed on a best-effort basis and should
@@ -619,7 +619,7 @@ export class MCPClient {
619
619
  */
620
620
  const transportsToTry: TransportType[] = this.transportType
621
621
  ? [this.transportType]
622
- : ['streamable_http', 'sse'];
622
+ : ['streamable-http', 'sse'];
623
623
 
624
624
  let lastError: unknown;
625
625
  let tokensExchanged = false;
@@ -1020,7 +1020,7 @@ export class MCPClient {
1020
1020
  );
1021
1021
 
1022
1022
  // Use default logic to get transport, defaulting to what's stored or auto
1023
- const tt = this.transportType || 'streamable_http';
1023
+ const tt = this.transportType || 'streamable-http';
1024
1024
  this.transport = this.getTransport(tt);
1025
1025
 
1026
1026
  await this.client.connect(this.transport);
@@ -1041,7 +1041,7 @@ export class MCPClient {
1041
1041
  await (this.oauthProvider as any).invalidateCredentials('all');
1042
1042
  }
1043
1043
 
1044
- await storage.removeSession(this.identity, this.sessionId);
1044
+ await sessions.delete(this.userId, this.sessionId);
1045
1045
  this.disconnect();
1046
1046
  }
1047
1047
 
@@ -1119,10 +1119,10 @@ export class MCPClient {
1119
1119
 
1120
1120
  /**
1121
1121
  * Gets the transport type being used
1122
- * @returns Transport type (defaults to 'streamable_http')
1122
+ * @returns Transport type (defaults to 'streamable-http')
1123
1123
  */
1124
1124
  getTransportType(): TransportType {
1125
- return this.transportType || 'streamable_http';
1125
+ return this.transportType || 'streamable-http';
1126
1126
  }
1127
1127
 
1128
1128
  /**
@@ -1156,16 +1156,16 @@ export class MCPClient {
1156
1156
  * Gets MCP server configuration for all active user sessions
1157
1157
  * Loads sessions from Redis, validates OAuth tokens, refreshes if expired
1158
1158
  * Returns ready-to-use configuration with valid auth headers
1159
- * @param identity - User ID to fetch sessions for
1159
+ * @param userId - User ID to fetch sessions for
1160
1160
  * @returns Object keyed by sanitized server labels containing transport, url, headers, etc.
1161
1161
  * @static
1162
1162
  */
1163
- static async getMcpServerConfig(identity: string): Promise<Record<string, any>> {
1163
+ static async getMcpServerConfig(userId: string): Promise<Record<string, any>> {
1164
1164
  const mcpConfig: Record<string, any> = {};
1165
- const sessions = await storage.getIdentitySessionsData(identity);
1165
+ const sessionList = await sessions.list(userId);
1166
1166
 
1167
1167
  await Promise.all(
1168
- sessions.map(async (sessionData) => {
1168
+ sessionList.map(async (sessionData) => {
1169
1169
  const { sessionId } = sessionData;
1170
1170
 
1171
1171
  try {
@@ -1176,16 +1176,16 @@ export class MCPClient {
1176
1176
  !sessionData.serverUrl ||
1177
1177
  !sessionData.callbackUrl
1178
1178
  ) {
1179
- await storage.removeSession(identity, sessionId);
1179
+ await sessions.delete(userId, sessionId);
1180
1180
  return;
1181
1181
  }
1182
1182
 
1183
1183
  // Get OAuth headers if session requires authentication
1184
1184
  let headers: Record<string, string> | undefined;
1185
1185
  try {
1186
- // Inject existing session data to avoid redundant storage reads in initialize()
1186
+ // Inject existing session data to avoid redundant session store reads in initialize()
1187
1187
  const client = new MCPClient({
1188
- identity,
1188
+ userId,
1189
1189
  sessionId,
1190
1190
  serverId: sessionData.serverId,
1191
1191
  serverUrl: sessionData.serverUrl,
@@ -1223,7 +1223,7 @@ export class MCPClient {
1223
1223
  ...(headers && { headers }),
1224
1224
  };
1225
1225
  } catch (error) {
1226
- await storage.removeSession(identity, sessionId);
1226
+ await sessions.delete(userId, sessionId);
1227
1227
  console.warn(`[MCP] Failed to process session ${sessionId}:`, error);
1228
1228
  }
1229
1229
  })
@@ -5,7 +5,7 @@ import type {
5
5
  OAuthClientMetadata,
6
6
  OAuthTokens
7
7
  } from "@modelcontextprotocol/sdk/shared/auth.js";
8
- import { storage, SessionData } from "../storage/index.js";
8
+ import { sessions, type Session } from "../storage/index.js";
9
9
  import {
10
10
  DEFAULT_CLIENT_NAME,
11
11
  DEFAULT_CLIENT_URI,
@@ -34,7 +34,7 @@ export interface AgentsOAuthProvider extends OAuthClientProvider {
34
34
  }
35
35
 
36
36
  export interface StorageOAuthClientProviderOptions {
37
- identity: string;
37
+ userId: string;
38
38
  serverId: string;
39
39
  sessionId: string;
40
40
  redirectUrl: string;
@@ -49,10 +49,10 @@ export interface StorageOAuthClientProviderOptions {
49
49
 
50
50
  /**
51
51
  * Storage-backed OAuth provider implementation for MCP
52
- * Stores OAuth tokens, client information, and PKCE verifiers using the configured StorageBackend
52
+ * Stores OAuth tokens, client information, and PKCE verifiers using the configured SessionStore
53
53
  */
54
54
  export class StorageOAuthClientProvider implements AgentsOAuthProvider {
55
- public readonly identity: string;
55
+ public readonly userId: string;
56
56
  public readonly serverId: string;
57
57
  public readonly sessionId: string;
58
58
  public readonly redirectUrl: string;
@@ -69,11 +69,11 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
69
69
  private tokenExpiresAt?: number;
70
70
 
71
71
  /**
72
- * Creates a new storage-backed OAuth provider
72
+ * Creates a new session-backed OAuth provider
73
73
  * @param options - Provider configuration
74
74
  */
75
75
  constructor(options: StorageOAuthClientProviderOptions) {
76
- this.identity = options.identity;
76
+ this.userId = options.userId;
77
77
  this.serverId = options.serverId;
78
78
  this.sessionId = options.sessionId;
79
79
  this.redirectUrl = options.redirectUrl;
@@ -110,25 +110,25 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
110
110
  }
111
111
 
112
112
  /**
113
- * Loads OAuth data from storage session
113
+ * Loads OAuth data from the session store
114
114
  * @private
115
115
  */
116
- private async getSessionData(): Promise<SessionData> {
117
- const data = await storage.getSession(this.identity, this.sessionId);
116
+ private async getSessionData(): Promise<Session> {
117
+ const data = await sessions.get(this.userId, this.sessionId);
118
118
  if (!data) {
119
- return {} as SessionData;
119
+ return {} as Session;
120
120
  }
121
121
  return data;
122
122
  }
123
123
 
124
124
  /**
125
- * Saves OAuth data to storage
125
+ * Saves OAuth data to the session store
126
126
  * @param data - Partial OAuth data to save
127
127
  * @private
128
128
  * @throws Error if session doesn't exist (session must be created by controller layer)
129
129
  */
130
- private async saveSessionData(data: Partial<SessionData>): Promise<void> {
131
- await storage.updateSession(this.identity, this.sessionId, data);
130
+ private async saveSessionData(data: Partial<Session>): Promise<void> {
131
+ await sessions.update(this.userId, this.sessionId, data);
132
132
  }
133
133
 
134
134
  /**
@@ -170,7 +170,7 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
170
170
  * Stores OAuth tokens
171
171
  */
172
172
  async saveTokens(tokens: OAuthTokens): Promise<void> {
173
- const data: Partial<SessionData> = { tokens };
173
+ const data: Partial<Session> = { tokens };
174
174
 
175
175
  if (tokens.expires_in) {
176
176
  this.tokenExpiresAt = Date.now() + (tokens.expires_in * 1000) - TOKEN_EXPIRY_BUFFER_MS;
@@ -188,7 +188,7 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
188
188
  }
189
189
 
190
190
  async checkState(_state: string): Promise<{ valid: boolean; serverId?: string; error?: string }> {
191
- const data = await storage.getSession(this.identity, this.sessionId);
191
+ const data = await sessions.get(this.userId, this.sessionId);
192
192
 
193
193
  if (!data) {
194
194
  return { valid: false, error: "Session not found" };
@@ -212,9 +212,9 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
212
212
  scope: "all" | "client" | "tokens" | "verifier"
213
213
  ): Promise<void> {
214
214
  if (scope === "all") {
215
- await storage.removeSession(this.identity, this.sessionId);
215
+ await sessions.delete(this.userId, this.sessionId);
216
216
  } else {
217
- const updates: Partial<SessionData> = {};
217
+ const updates: Partial<Session> = {};
218
218
 
219
219
  if (scope === "client") {
220
220
  updates.clientInformation = undefined;
@@ -1,15 +1,15 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import * as path from 'path';
3
- import { StorageBackend, SessionData, SetClientOptions } from './types.js';
3
+ import type { SessionStore, Session, SetClientOptions } from './types.js';
4
4
  import { generateSessionId } from '../../shared/utils.js';
5
5
 
6
6
  /**
7
- * File system implementation of StorageBackend
7
+ * File system implementation of SessionStore
8
8
  * Persists sessions to a JSON file
9
9
  */
10
- export class FileStorageBackend implements StorageBackend {
10
+ export class FileStorageBackend implements SessionStore {
11
11
  private filePath: string;
12
- private memoryCache: Map<string, SessionData> | null = null;
12
+ private memoryCache: Map<string, Session> | null = null;
13
13
  private initialized = false;
14
14
 
15
15
  /**
@@ -36,8 +36,8 @@ export class FileStorageBackend implements StorageBackend {
36
36
 
37
37
  this.memoryCache = new Map();
38
38
  if (Array.isArray(json)) {
39
- json.forEach((s: SessionData) => {
40
- this.memoryCache!.set(this.getSessionKey(s.identity || 'unknown', s.sessionId), s);
39
+ json.forEach((s: Session) => {
40
+ this.memoryCache!.set(this.getSessionKey(s.userId || 'unknown', s.sessionId), s);
41
41
  });
42
42
  }
43
43
  } catch (error: any) {
@@ -65,20 +65,20 @@ export class FileStorageBackend implements StorageBackend {
65
65
  await fs.writeFile(this.filePath, JSON.stringify(sessions, null, 2), 'utf-8');
66
66
  }
67
67
 
68
- private getSessionKey(identity: string, sessionId: string): string {
69
- return `${identity}:${sessionId}`;
68
+ private getSessionKey(userId: string, sessionId: string): string {
69
+ return `${userId}:${sessionId}`;
70
70
  }
71
71
 
72
72
  generateSessionId(): string {
73
73
  return generateSessionId();
74
74
  }
75
75
 
76
- async createSession(session: SessionData, ttl?: number): Promise<void> {
76
+ async create(session: Session, ttl?: number): Promise<void> {
77
77
  await this.ensureInitialized();
78
- const { sessionId, identity } = session;
79
- if (!sessionId || !identity) throw new Error('identity and sessionId required');
78
+ const { sessionId, userId } = session;
79
+ if (!sessionId || !userId) throw new Error('userId and sessionId required');
80
80
 
81
- const sessionKey = this.getSessionKey(identity, sessionId);
81
+ const sessionKey = this.getSessionKey(userId, sessionId);
82
82
  if (this.memoryCache!.has(sessionKey)) {
83
83
  throw new Error(`Session ${sessionId} already exists`);
84
84
  }
@@ -88,11 +88,11 @@ export class FileStorageBackend implements StorageBackend {
88
88
  // Note: TTL is ignored in file backend - sessions don't auto-expire
89
89
  }
90
90
 
91
- async updateSession(identity: string, sessionId: string, data: Partial<SessionData>, ttl?: number): Promise<void> {
91
+ async update(userId: string, sessionId: string, data: Partial<Session>, ttl?: number): Promise<void> {
92
92
  await this.ensureInitialized();
93
- if (!identity || !sessionId) throw new Error('identity and sessionId required');
93
+ if (!userId || !sessionId) throw new Error('userId and sessionId required');
94
94
 
95
- const sessionKey = this.getSessionKey(identity, sessionId);
95
+ const sessionKey = this.getSessionKey(userId, sessionId);
96
96
  const current = this.memoryCache!.get(sessionKey);
97
97
 
98
98
  if (!current) {
@@ -109,33 +109,33 @@ export class FileStorageBackend implements StorageBackend {
109
109
  // Note: TTL is ignored in file backend - sessions don't auto-expire
110
110
  }
111
111
 
112
- async getSession(identity: string, sessionId: string): Promise<SessionData | null> {
112
+ async get(userId: string, sessionId: string): Promise<Session | null> {
113
113
  await this.ensureInitialized();
114
- const sessionKey = this.getSessionKey(identity, sessionId);
114
+ const sessionKey = this.getSessionKey(userId, sessionId);
115
115
  return this.memoryCache!.get(sessionKey) || null;
116
116
  }
117
117
 
118
- async getIdentitySessionsData(identity: string): Promise<SessionData[]> {
118
+ async list(userId: string): Promise<Session[]> {
119
119
  await this.ensureInitialized();
120
- return Array.from(this.memoryCache!.values()).filter(s => s.identity === identity);
120
+ return Array.from(this.memoryCache!.values()).filter(s => s.userId === userId);
121
121
  }
122
122
 
123
- async getIdentityMcpSessions(identity: string): Promise<string[]> {
123
+ async listIds(userId: string): Promise<string[]> {
124
124
  await this.ensureInitialized();
125
125
  return Array.from(this.memoryCache!.values())
126
- .filter(s => s.identity === identity)
126
+ .filter(s => s.userId === userId)
127
127
  .map(s => s.sessionId);
128
128
  }
129
129
 
130
- async removeSession(identity: string, sessionId: string): Promise<void> {
130
+ async delete(userId: string, sessionId: string): Promise<void> {
131
131
  await this.ensureInitialized();
132
- const sessionKey = this.getSessionKey(identity, sessionId);
132
+ const sessionKey = this.getSessionKey(userId, sessionId);
133
133
  if (this.memoryCache!.delete(sessionKey)) {
134
134
  await this.flush();
135
135
  }
136
136
  }
137
137
 
138
- async getAllSessionIds(): Promise<string[]> {
138
+ async listAllIds(): Promise<string[]> {
139
139
  await this.ensureInitialized();
140
140
  return Array.from(this.memoryCache!.values()).map(s => s.sessionId);
141
141
  }
@@ -146,7 +146,7 @@ export class FileStorageBackend implements StorageBackend {
146
146
  await this.flush();
147
147
  }
148
148
 
149
- async cleanupExpiredSessions(): Promise<void> {
149
+ async cleanupExpired(): Promise<void> {
150
150
  // Could implement TTL check here using createdAt
151
151
  await this.ensureInitialized();
152
152
  }
@@ -4,28 +4,53 @@ import { MemoryStorageBackend } from './memory-backend';
4
4
  import { FileStorageBackend } from './file-backend';
5
5
  import { SqliteStorage } from './sqlite-backend.js';
6
6
  import { SupabaseStorageBackend } from './supabase-backend.js';
7
- import type { StorageBackend } from './types.js';
7
+ import { NeonStorageBackend, type NeonStorageOptions } from './neon-backend.js';
8
+ import type { SessionStore } from './types.js';
8
9
 
9
10
  // Re-export types
10
11
  export * from './types.js';
11
12
  export { generateSessionId } from '../../shared/utils.js';
12
- export { RedisStorageBackend, MemoryStorageBackend, FileStorageBackend, SqliteStorage, SupabaseStorageBackend };
13
+ export { RedisStorageBackend, MemoryStorageBackend, FileStorageBackend, SqliteStorage, SupabaseStorageBackend, NeonStorageBackend };
13
14
 
14
15
  export function createSupabaseStorageBackend(client: any): SupabaseStorageBackend {
15
16
  return new SupabaseStorageBackend(client);
16
17
  }
17
18
 
18
- let storageInstance: StorageBackend | null = null;
19
- let storagePromise: Promise<StorageBackend> | null = null;
19
+ export function createNeonStorageBackend(sql: any, options?: NeonStorageOptions): NeonStorageBackend {
20
+ return new NeonStorageBackend(sql, options);
21
+ }
22
+
23
+ function warnIfNeonConnectionStringIsInsecure(connectionString: string): void {
24
+ try {
25
+ const url = new URL(connectionString);
26
+ const sslMode = url.searchParams.get('sslmode');
27
+ const channelBinding = url.searchParams.get('channel_binding');
28
+
29
+ if (!sslMode) {
30
+ console.warn('[mcp-ts][Storage] Neon connection string does not include sslmode. Neon recommends sslmode=verify-full for the strongest certificate verification.');
31
+ } else if (!['verify-full', 'require'].includes(sslMode)) {
32
+ console.warn(`[mcp-ts][Storage] Neon connection string uses sslmode=${sslMode}. Use sslmode=verify-full or sslmode=require for secure connections.`);
33
+ }
34
+
35
+ if (!channelBinding) {
36
+ console.warn('[mcp-ts][Storage] Neon connection string does not include channel_binding=require. Add it when supported by your runtime and connection path.');
37
+ }
38
+ } catch {
39
+ console.warn('[mcp-ts][Storage] Neon connection string could not be parsed for SSL checks.');
40
+ }
41
+ }
42
+
43
+ let storageInstance: SessionStore | null = null;
44
+ let storagePromise: Promise<SessionStore> | null = null;
20
45
 
21
- async function initializeStorage<T extends StorageBackend>(store: T): Promise<T> {
46
+ async function initializeStorage<T extends SessionStore>(store: T): Promise<T> {
22
47
  if (typeof store.init === 'function') {
23
48
  await store.init();
24
49
  }
25
50
  return store;
26
51
  }
27
52
 
28
- async function createStorage(): Promise<StorageBackend> {
53
+ async function createStorage(): Promise<SessionStore> {
29
54
  const type = process.env.MCP_TS_STORAGE_TYPE?.toLowerCase();
30
55
 
31
56
  // Explicit selection
@@ -80,6 +105,26 @@ async function createStorage(): Promise<StorageBackend> {
80
105
  }
81
106
  }
82
107
 
108
+ if (type === 'neon') {
109
+ const connectionString = process.env.NEON_DATABASE_URL || process.env.DATABASE_URL;
110
+
111
+ if (!connectionString) {
112
+ console.warn('[mcp-ts][Storage] Explicit selection "neon" requires NEON_DATABASE_URL or DATABASE_URL.');
113
+ } else {
114
+ try {
115
+ const { neon } = await import('@neondatabase/serverless');
116
+ warnIfNeonConnectionStringIsInsecure(connectionString);
117
+ const sql = neon(connectionString);
118
+ console.log('[mcp-ts][Storage] Explicit selection: "neon"');
119
+ return await initializeStorage(new NeonStorageBackend(sql));
120
+ } catch (error: any) {
121
+ console.error('[mcp-ts][Storage] Failed to initialize Neon:', error.message);
122
+ console.log('[mcp-ts][Storage] Falling back to In-Memory storage');
123
+ return await initializeStorage(new MemoryStorageBackend());
124
+ }
125
+ }
126
+ }
127
+
83
128
  if (type === 'memory') {
84
129
  console.log('[mcp-ts][Storage] Explicit selection: "memory"');
85
130
  return await initializeStorage(new MemoryStorageBackend());
@@ -126,11 +171,23 @@ async function createStorage(): Promise<StorageBackend> {
126
171
  }
127
172
  }
128
173
 
174
+ if (process.env.NEON_DATABASE_URL) {
175
+ try {
176
+ const { neon } = await import('@neondatabase/serverless');
177
+ warnIfNeonConnectionStringIsInsecure(process.env.NEON_DATABASE_URL);
178
+ const sql = neon(process.env.NEON_DATABASE_URL);
179
+ console.log('[mcp-ts][Storage] Auto-detection: "neon" (via NEON_DATABASE_URL)');
180
+ return await initializeStorage(new NeonStorageBackend(sql));
181
+ } catch (error: any) {
182
+ console.error('[mcp-ts][Storage] Neon auto-detection failed:', error.message);
183
+ }
184
+ }
185
+
129
186
  console.log('[mcp-ts][Storage] Defaulting to: "memory"');
130
187
  return await initializeStorage(new MemoryStorageBackend());
131
188
  }
132
189
 
133
- async function getStorage(): Promise<StorageBackend> {
190
+ async function getStorage(): Promise<SessionStore> {
134
191
  if (storageInstance) {
135
192
  return storageInstance;
136
193
  }
@@ -149,9 +206,9 @@ async function getStorage(): Promise<StorageBackend> {
149
206
  /**
150
207
  * Set the storage instance (for testing)
151
208
  * @internal
152
- * @param instance - StorageBackend instance or null to reset
209
+ * @param instance - SessionStore instance or null to reset
153
210
  */
154
- export function _setStorageInstanceForTesting(instance: StorageBackend | null): void {
211
+ export function _setStorageInstanceForTesting(instance: SessionStore | null): void {
155
212
  storageInstance = instance;
156
213
  if (!instance) {
157
214
  storagePromise = null;
@@ -162,7 +219,7 @@ export function _setStorageInstanceForTesting(instance: StorageBackend | null):
162
219
  * Global session store instance
163
220
  * Uses lazy initialization with a Proxy to handle async setup transparently
164
221
  */
165
- export const storage: StorageBackend = new Proxy({} as StorageBackend, {
222
+ export const sessions: SessionStore = new Proxy({} as SessionStore, {
166
223
  get(_target, prop) {
167
224
  return async (...args: any[]) => {
168
225
  const instance = await getStorage();