@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.
- package/README.md +12 -6
- package/dist/adapters/agui-adapter.d.mts +3 -3
- package/dist/adapters/agui-adapter.d.ts +3 -3
- package/dist/adapters/agui-adapter.js +4 -5
- package/dist/adapters/agui-adapter.js.map +1 -1
- package/dist/adapters/agui-adapter.mjs +4 -5
- package/dist/adapters/agui-adapter.mjs.map +1 -1
- package/dist/adapters/agui-middleware.d.mts +3 -3
- package/dist/adapters/agui-middleware.d.ts +3 -3
- package/dist/adapters/ai-adapter.d.mts +9 -3
- package/dist/adapters/ai-adapter.d.ts +9 -3
- package/dist/adapters/ai-adapter.js +20 -6
- package/dist/adapters/ai-adapter.js.map +1 -1
- package/dist/adapters/ai-adapter.mjs +20 -6
- package/dist/adapters/ai-adapter.mjs.map +1 -1
- package/dist/adapters/langchain-adapter.d.mts +3 -3
- package/dist/adapters/langchain-adapter.d.ts +3 -3
- package/dist/adapters/langchain-adapter.js +9 -6
- package/dist/adapters/langchain-adapter.js.map +1 -1
- package/dist/adapters/langchain-adapter.mjs +9 -6
- package/dist/adapters/langchain-adapter.mjs.map +1 -1
- package/dist/adapters/mastra-adapter.d.mts +1 -1
- package/dist/adapters/mastra-adapter.d.ts +1 -1
- package/dist/adapters/mastra-adapter.js +5 -1
- package/dist/adapters/mastra-adapter.js.map +1 -1
- package/dist/adapters/mastra-adapter.mjs +5 -1
- package/dist/adapters/mastra-adapter.mjs.map +1 -1
- package/dist/bin/mcp-ts.js +7 -1
- package/dist/bin/mcp-ts.js.map +1 -1
- package/dist/bin/mcp-ts.mjs +7 -1
- package/dist/bin/mcp-ts.mjs.map +1 -1
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +9 -13
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +9 -13
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +7 -7
- package/dist/client/react.d.ts +7 -7
- package/dist/client/react.js +111 -63
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +111 -63
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +7 -7
- package/dist/client/vue.d.ts +7 -7
- package/dist/client/vue.js +14 -18
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +14 -18
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{index-DhA-OEAe.d.ts → index-C9gvpxy5.d.ts} +5 -5
- package/dist/{index-bFL4ZF2N.d.mts → index-eaH14_5u.d.mts} +5 -5
- package/dist/index.d.mts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +616 -370
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +615 -370
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-CHE8QpVE.d.ts → multi-session-client-BYtguGJm.d.ts} +22 -22
- package/dist/{multi-session-client-CQsRbxYI.d.mts → multi-session-client-DYNe6az3.d.mts} +22 -22
- package/dist/server/index.d.mts +31 -34
- package/dist/server/index.d.ts +31 -34
- package/dist/server/index.js +531 -256
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +530 -256
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +5 -5
- package/dist/shared/index.d.ts +5 -5
- package/dist/shared/index.js +76 -101
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +76 -101
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{tool-router-Dh2804tM.d.ts → tool-router-Ddtybmr0.d.ts} +71 -73
- package/dist/{tool-router-BVaV1udm.d.mts → tool-router-Dnd6IOKC.d.mts} +71 -73
- package/dist/{types-rIuN1CQi.d.mts → types-BCAG20P6.d.mts} +4 -4
- package/dist/{types-rIuN1CQi.d.ts → types-BCAG20P6.d.ts} +4 -4
- package/dist/{utils-0qmYrqoa.d.mts → utils-DELRKQPU.d.mts} +1 -1
- package/dist/{utils-0qmYrqoa.d.ts → utils-DELRKQPU.d.ts} +1 -1
- package/migrations/neon/20260513010000_install_mcp_sessions.sql +69 -0
- package/migrations/neon/20260513020000_add_session_cleanup_cron.sql +35 -0
- package/{supabase/migrations → migrations/supabase}/20260330195700_install_mcp_sessions.sql +7 -9
- package/package.json +14 -5
- package/src/adapters/ai-adapter.ts +30 -1
- package/src/adapters/langchain-adapter.ts +6 -2
- package/src/adapters/mastra-adapter.ts +6 -2
- package/src/bin/mcp-ts.ts +8 -1
- package/src/client/core/app-host.ts +1 -1
- package/src/client/core/sse-client.ts +12 -14
- package/src/client/core/types.ts +1 -1
- package/src/client/react/oauth-popup.tsx +111 -51
- package/src/client/react/use-mcp-apps.tsx +1 -1
- package/src/client/react/use-mcp.ts +11 -11
- package/src/client/vue/use-mcp.ts +10 -10
- package/src/server/handlers/nextjs-handler.ts +18 -15
- package/src/server/handlers/sse-handler.ts +29 -29
- package/src/server/index.ts +1 -1
- package/src/server/mcp/multi-session-client.ts +17 -17
- package/src/server/mcp/oauth-client.ts +37 -37
- package/src/server/mcp/storage-oauth-provider.ts +17 -17
- package/src/server/storage/file-backend.ts +25 -25
- package/src/server/storage/index.ts +67 -10
- package/src/server/storage/memory-backend.ts +34 -34
- package/src/server/storage/neon-backend.ts +281 -0
- package/src/server/storage/redis-backend.ts +64 -64
- package/src/server/storage/sqlite-backend.ts +33 -33
- package/src/server/storage/supabase-backend.ts +23 -24
- package/src/server/storage/types.ts +18 -21
- package/src/shared/errors.ts +1 -1
- package/src/shared/index.ts +1 -2
- package/src/shared/meta-tools.ts +4 -6
- package/src/shared/schema-compressor.ts +2 -42
- package/src/shared/tool-index.ts +89 -84
- package/src/shared/tool-router.ts +0 -24
- package/src/shared/types.ts +4 -4
- /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 {
|
|
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' | '
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
356
|
+
await sessions.create({
|
|
357
357
|
sessionId: this.sessionId,
|
|
358
|
-
|
|
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 || '
|
|
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
|
|
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
|
-
|
|
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 || '
|
|
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
|
|
400
|
+
const existingSession = await sessions.get(this.userId, this.sessionId);
|
|
401
401
|
if (existingSession) {
|
|
402
|
-
await
|
|
402
|
+
await sessions.update(this.userId, this.sessionId, sessionData, ttl);
|
|
403
403
|
} else {
|
|
404
|
-
await
|
|
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
|
-
: ['
|
|
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
|
|
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
|
|
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
|
|
582
|
+
const existingSession = await sessions.get(this.userId, this.sessionId);
|
|
583
583
|
if (!existingSession || existingSession.active !== true) {
|
|
584
|
-
await
|
|
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
|
-
: ['
|
|
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 || '
|
|
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
|
|
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 '
|
|
1122
|
+
* @returns Transport type (defaults to 'streamable-http')
|
|
1123
1123
|
*/
|
|
1124
1124
|
getTransportType(): TransportType {
|
|
1125
|
-
return this.transportType || '
|
|
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
|
|
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(
|
|
1163
|
+
static async getMcpServerConfig(userId: string): Promise<Record<string, any>> {
|
|
1164
1164
|
const mcpConfig: Record<string, any> = {};
|
|
1165
|
-
const
|
|
1165
|
+
const sessionList = await sessions.list(userId);
|
|
1166
1166
|
|
|
1167
1167
|
await Promise.all(
|
|
1168
|
-
|
|
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
|
|
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
|
|
1186
|
+
// Inject existing session data to avoid redundant session store reads in initialize()
|
|
1187
1187
|
const client = new MCPClient({
|
|
1188
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
72
|
+
* Creates a new session-backed OAuth provider
|
|
73
73
|
* @param options - Provider configuration
|
|
74
74
|
*/
|
|
75
75
|
constructor(options: StorageOAuthClientProviderOptions) {
|
|
76
|
-
this.
|
|
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
|
|
113
|
+
* Loads OAuth data from the session store
|
|
114
114
|
* @private
|
|
115
115
|
*/
|
|
116
|
-
private async getSessionData(): Promise<
|
|
117
|
-
const data = await
|
|
116
|
+
private async getSessionData(): Promise<Session> {
|
|
117
|
+
const data = await sessions.get(this.userId, this.sessionId);
|
|
118
118
|
if (!data) {
|
|
119
|
-
return {} as
|
|
119
|
+
return {} as Session;
|
|
120
120
|
}
|
|
121
121
|
return data;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
* Saves OAuth data to
|
|
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<
|
|
131
|
-
await
|
|
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<
|
|
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
|
|
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
|
|
215
|
+
await sessions.delete(this.userId, this.sessionId);
|
|
216
216
|
} else {
|
|
217
|
-
const updates: Partial<
|
|
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 {
|
|
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
|
|
7
|
+
* File system implementation of SessionStore
|
|
8
8
|
* Persists sessions to a JSON file
|
|
9
9
|
*/
|
|
10
|
-
export class FileStorageBackend implements
|
|
10
|
+
export class FileStorageBackend implements SessionStore {
|
|
11
11
|
private filePath: string;
|
|
12
|
-
private memoryCache: Map<string,
|
|
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:
|
|
40
|
-
this.memoryCache!.set(this.getSessionKey(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(
|
|
69
|
-
return `${
|
|
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
|
|
76
|
+
async create(session: Session, ttl?: number): Promise<void> {
|
|
77
77
|
await this.ensureInitialized();
|
|
78
|
-
const { sessionId,
|
|
79
|
-
if (!sessionId || !
|
|
78
|
+
const { sessionId, userId } = session;
|
|
79
|
+
if (!sessionId || !userId) throw new Error('userId and sessionId required');
|
|
80
80
|
|
|
81
|
-
const sessionKey = this.getSessionKey(
|
|
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
|
|
91
|
+
async update(userId: string, sessionId: string, data: Partial<Session>, ttl?: number): Promise<void> {
|
|
92
92
|
await this.ensureInitialized();
|
|
93
|
-
if (!
|
|
93
|
+
if (!userId || !sessionId) throw new Error('userId and sessionId required');
|
|
94
94
|
|
|
95
|
-
const sessionKey = this.getSessionKey(
|
|
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
|
|
112
|
+
async get(userId: string, sessionId: string): Promise<Session | null> {
|
|
113
113
|
await this.ensureInitialized();
|
|
114
|
-
const sessionKey = this.getSessionKey(
|
|
114
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
115
115
|
return this.memoryCache!.get(sessionKey) || null;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
async
|
|
118
|
+
async list(userId: string): Promise<Session[]> {
|
|
119
119
|
await this.ensureInitialized();
|
|
120
|
-
return Array.from(this.memoryCache!.values()).filter(s => s.
|
|
120
|
+
return Array.from(this.memoryCache!.values()).filter(s => s.userId === userId);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
async
|
|
123
|
+
async listIds(userId: string): Promise<string[]> {
|
|
124
124
|
await this.ensureInitialized();
|
|
125
125
|
return Array.from(this.memoryCache!.values())
|
|
126
|
-
.filter(s => s.
|
|
126
|
+
.filter(s => s.userId === userId)
|
|
127
127
|
.map(s => s.sessionId);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
async
|
|
130
|
+
async delete(userId: string, sessionId: string): Promise<void> {
|
|
131
131
|
await this.ensureInitialized();
|
|
132
|
-
const sessionKey = this.getSessionKey(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
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<
|
|
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<
|
|
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 -
|
|
209
|
+
* @param instance - SessionStore instance or null to reset
|
|
153
210
|
*/
|
|
154
|
-
export function _setStorageInstanceForTesting(instance:
|
|
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
|
|
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();
|