@mcp-ts/sdk 1.6.2 → 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 +15 -19
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +15 -19
- 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/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
|
@@ -26,7 +26,7 @@ import type {
|
|
|
26
26
|
SessionListResult,
|
|
27
27
|
ConnectResult,
|
|
28
28
|
DisconnectResult,
|
|
29
|
-
|
|
29
|
+
GetSessionResult,
|
|
30
30
|
FinishAuthResult,
|
|
31
31
|
ListToolsRpcResult,
|
|
32
32
|
ListPromptsResult,
|
|
@@ -37,7 +37,7 @@ import { RpcErrorCodes } from '../../shared/errors.js';
|
|
|
37
37
|
import { UnauthorizedError } from '../../shared/errors.js';
|
|
38
38
|
import { isConnectionEvent, isRpcResponseEvent } from '../../shared/event-routing.js';
|
|
39
39
|
import { MCPClient } from '../mcp/oauth-client.js';
|
|
40
|
-
import {
|
|
40
|
+
import { sessions } from '../storage/index.js';
|
|
41
41
|
|
|
42
42
|
// ============================================
|
|
43
43
|
// Types & Interfaces
|
|
@@ -52,10 +52,10 @@ export interface ClientMetadata {
|
|
|
52
52
|
|
|
53
53
|
export interface SSEHandlerOptions {
|
|
54
54
|
/** User/Client identifier */
|
|
55
|
-
|
|
55
|
+
userId: string;
|
|
56
56
|
|
|
57
57
|
/** Optional callback for authentication/authorization */
|
|
58
|
-
onAuth?: (
|
|
58
|
+
onAuth?: (userId: string) => Promise<boolean>;
|
|
59
59
|
|
|
60
60
|
/** Heartbeat interval in milliseconds @default 30000 */
|
|
61
61
|
heartbeatInterval?: number;
|
|
@@ -92,7 +92,7 @@ function normalizeHeaders(headers?: Record<string, string>): Record<string, stri
|
|
|
92
92
|
* Each instance corresponds to one connected browser client.
|
|
93
93
|
*/
|
|
94
94
|
export class SSEConnectionManager {
|
|
95
|
-
private readonly
|
|
95
|
+
private readonly userId: string;
|
|
96
96
|
private readonly clients = new Map<string, MCPClient>();
|
|
97
97
|
private heartbeatTimer?: NodeJS.Timeout;
|
|
98
98
|
private isActive = true;
|
|
@@ -101,7 +101,7 @@ export class SSEConnectionManager {
|
|
|
101
101
|
private readonly options: SSEHandlerOptions,
|
|
102
102
|
private readonly sendEvent: (event: McpConnectionEvent | McpObservabilityEvent | McpRpcResponse) => void
|
|
103
103
|
) {
|
|
104
|
-
this.
|
|
104
|
+
this.userId = options.userId;
|
|
105
105
|
this.startHeartbeat();
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -148,11 +148,11 @@ export class SSEConnectionManager {
|
|
|
148
148
|
*/
|
|
149
149
|
async handleRequest(request: McpRpcRequest): Promise<McpRpcResponse> {
|
|
150
150
|
try {
|
|
151
|
-
let result: SessionListResult | ConnectResult | DisconnectResult |
|
|
151
|
+
let result: SessionListResult | ConnectResult | DisconnectResult | GetSessionResult | FinishAuthResult | ListToolsRpcResult | ListPromptsResult | ListResourcesResult | unknown;
|
|
152
152
|
|
|
153
153
|
switch (request.method) {
|
|
154
|
-
case '
|
|
155
|
-
result = await this.
|
|
154
|
+
case 'listSessions':
|
|
155
|
+
result = await this.listSessions();
|
|
156
156
|
break;
|
|
157
157
|
|
|
158
158
|
case 'connect':
|
|
@@ -171,8 +171,8 @@ export class SSEConnectionManager {
|
|
|
171
171
|
result = await this.callTool(request.params as CallToolParams);
|
|
172
172
|
break;
|
|
173
173
|
|
|
174
|
-
case '
|
|
175
|
-
result = await this.
|
|
174
|
+
case 'getSession':
|
|
175
|
+
result = await this.getSession(request.params as SessionParams);
|
|
176
176
|
break;
|
|
177
177
|
|
|
178
178
|
case 'finishAuth':
|
|
@@ -225,13 +225,13 @@ export class SSEConnectionManager {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
/**
|
|
228
|
-
* Get all sessions for the current
|
|
228
|
+
* Get all sessions for the current userId
|
|
229
229
|
*/
|
|
230
|
-
private async
|
|
231
|
-
const
|
|
230
|
+
private async listSessions(): Promise<SessionListResult> {
|
|
231
|
+
const sessionList = await sessions.list(this.userId);
|
|
232
232
|
|
|
233
233
|
return {
|
|
234
|
-
sessions:
|
|
234
|
+
sessions: sessionList.map((s) => ({
|
|
235
235
|
sessionId: s.sessionId,
|
|
236
236
|
serverId: s.serverId,
|
|
237
237
|
serverName: s.serverName,
|
|
@@ -254,10 +254,10 @@ export class SSEConnectionManager {
|
|
|
254
254
|
// Tool name format: tool_<serverId>_<toolName> - with 12 char serverId leaves 46 chars for tool name
|
|
255
255
|
const serverId = params.serverId && params.serverId.length <= 12
|
|
256
256
|
? params.serverId
|
|
257
|
-
: await
|
|
257
|
+
: await sessions.generateSessionId();
|
|
258
258
|
|
|
259
259
|
// Check for existing connections
|
|
260
|
-
const existingSessions = await
|
|
260
|
+
const existingSessions = await sessions.list(this.userId);
|
|
261
261
|
const duplicate = existingSessions.find(s =>
|
|
262
262
|
s.serverId === serverId || s.serverUrl === serverUrl
|
|
263
263
|
);
|
|
@@ -266,7 +266,7 @@ export class SSEConnectionManager {
|
|
|
266
266
|
// If the existing session is still pending OAuth, treat connect as "resume auth"
|
|
267
267
|
// instead of failing with duplicate connection error.
|
|
268
268
|
if (duplicate.active === false) {
|
|
269
|
-
await this.
|
|
269
|
+
await this.getSession({ sessionId: duplicate.sessionId });
|
|
270
270
|
return {
|
|
271
271
|
sessionId: duplicate.sessionId,
|
|
272
272
|
success: true,
|
|
@@ -276,7 +276,7 @@ export class SSEConnectionManager {
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
// Generate session ID
|
|
279
|
-
const sessionId = await
|
|
279
|
+
const sessionId = await sessions.generateSessionId();
|
|
280
280
|
|
|
281
281
|
try {
|
|
282
282
|
// Get resolved client metadata
|
|
@@ -284,7 +284,7 @@ export class SSEConnectionManager {
|
|
|
284
284
|
|
|
285
285
|
// Create MCP client
|
|
286
286
|
const client = new MCPClient({
|
|
287
|
-
|
|
287
|
+
userId: this.userId,
|
|
288
288
|
sessionId,
|
|
289
289
|
serverId,
|
|
290
290
|
serverName,
|
|
@@ -360,7 +360,7 @@ export class SSEConnectionManager {
|
|
|
360
360
|
} else {
|
|
361
361
|
// Handle orphaned sessions (e.g., OAuth flow failed before client was stored)
|
|
362
362
|
// Directly remove from storage since there's no active client
|
|
363
|
-
await
|
|
363
|
+
await sessions.delete(this.userId, sessionId);
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
return { success: true };
|
|
@@ -375,13 +375,13 @@ export class SSEConnectionManager {
|
|
|
375
375
|
return existing;
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
const session = await
|
|
378
|
+
const session = await sessions.get(this.userId, sessionId);
|
|
379
379
|
if (!session) {
|
|
380
380
|
throw new Error('Session not found');
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
const client = new MCPClient({
|
|
384
|
-
|
|
384
|
+
userId: this.userId,
|
|
385
385
|
sessionId,
|
|
386
386
|
// These fields are optional in MCPClient, but when rehydrating a known
|
|
387
387
|
// stored session on the server we pass them explicitly to preserve the
|
|
@@ -439,10 +439,10 @@ export class SSEConnectionManager {
|
|
|
439
439
|
/**
|
|
440
440
|
* Restore and validate an existing session
|
|
441
441
|
*/
|
|
442
|
-
private async
|
|
442
|
+
private async getSession(params: SessionParams): Promise<GetSessionResult> {
|
|
443
443
|
const { sessionId } = params;
|
|
444
444
|
|
|
445
|
-
const session = await
|
|
445
|
+
const session = await sessions.get(this.userId, sessionId);
|
|
446
446
|
if (!session) {
|
|
447
447
|
throw new Error('Session not found');
|
|
448
448
|
}
|
|
@@ -462,7 +462,7 @@ export class SSEConnectionManager {
|
|
|
462
462
|
const clientMetadata = await this.getResolvedClientMetadata();
|
|
463
463
|
|
|
464
464
|
const client = new MCPClient({
|
|
465
|
-
|
|
465
|
+
userId: this.userId,
|
|
466
466
|
sessionId,
|
|
467
467
|
// These fields are optional in MCPClient, but when rehydrating a known
|
|
468
468
|
// stored session on the server we pass them explicitly to preserve the
|
|
@@ -506,14 +506,14 @@ export class SSEConnectionManager {
|
|
|
506
506
|
private async finishAuth(params: FinishAuthParams): Promise<FinishAuthResult> {
|
|
507
507
|
const { sessionId, code } = params;
|
|
508
508
|
|
|
509
|
-
const session = await
|
|
509
|
+
const session = await sessions.get(this.userId, sessionId);
|
|
510
510
|
if (!session) {
|
|
511
511
|
throw new Error('Session not found');
|
|
512
512
|
}
|
|
513
513
|
|
|
514
514
|
try {
|
|
515
515
|
const client = new MCPClient({
|
|
516
|
-
|
|
516
|
+
userId: this.userId,
|
|
517
517
|
sessionId,
|
|
518
518
|
// These fields are optional in MCPClient, but when rehydrating a known
|
|
519
519
|
// stored session on the server we pass them explicitly to preserve the
|
|
@@ -524,7 +524,7 @@ export class SSEConnectionManager {
|
|
|
524
524
|
serverUrl: session.serverUrl,
|
|
525
525
|
callbackUrl: session.callbackUrl,
|
|
526
526
|
// NOTE: transportType is intentionally omitted here.
|
|
527
|
-
// The session's stored transportType is a placeholder ('
|
|
527
|
+
// The session's stored transportType is a placeholder ('streamable-http')
|
|
528
528
|
// set before transport negotiation. Omitting it lets MCPClient auto-negotiate
|
|
529
529
|
// (try streamable_http → SSE fallback), which is critical for servers like
|
|
530
530
|
// Neon that only support SSE transport.
|
package/src/server/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
/** Core MCP client and session management */
|
|
7
7
|
export { MCPClient } from './mcp/oauth-client.js';
|
|
8
8
|
export { UnauthorizedError } from '../shared/errors.js';
|
|
9
|
-
export {
|
|
9
|
+
export { sessions, type SessionStore } from './storage/index.js';
|
|
10
10
|
export { StorageOAuthClientProvider } from './mcp/storage-oauth-provider.js';
|
|
11
11
|
export { MultiSessionClient } from './mcp/multi-session-client.js';
|
|
12
12
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
3
|
import { MCPClient } from './oauth-client.js';
|
|
4
|
-
import {
|
|
4
|
+
import { sessions, type Session } from '../storage/index.js';
|
|
5
5
|
|
|
6
6
|
const DEFAULT_TIMEOUT_MS = 15000;
|
|
7
7
|
const DEFAULT_MAX_RETRIES = 2;
|
|
@@ -9,7 +9,7 @@ const DEFAULT_RETRY_DELAY_MS = 1000;
|
|
|
9
9
|
const CONNECTION_BATCH_SIZE = 5;
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Manages multiple MCP connections for a single user
|
|
12
|
+
* Manages multiple MCP connections for a single user.
|
|
13
13
|
* Allows aggregating tools from all connected servers.
|
|
14
14
|
*/
|
|
15
15
|
export interface MultiSessionOptions {
|
|
@@ -31,7 +31,7 @@ export interface MultiSessionOptions {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Manages multiple MCP client connections for a single user
|
|
34
|
+
* Manages multiple MCP client connections for a single user.
|
|
35
35
|
*
|
|
36
36
|
* On a traditional long-running server, you can cache this instance per user
|
|
37
37
|
* so the connections stay alive between requests. On serverless, a new instance
|
|
@@ -40,18 +40,18 @@ export interface MultiSessionOptions {
|
|
|
40
40
|
*/
|
|
41
41
|
export class MultiSessionClient {
|
|
42
42
|
private clients: MCPClient[] = [];
|
|
43
|
-
private
|
|
43
|
+
private userId: string;
|
|
44
44
|
private options: MultiSessionOptions;
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* Creates a new MultiSessionClient for the given user
|
|
47
|
+
* Creates a new MultiSessionClient for the given user userId.
|
|
48
48
|
*
|
|
49
|
-
* @param
|
|
49
|
+
* @param userId - A unique string identifying the user (e.g. user ID or email).
|
|
50
50
|
* @param options - Optional tuning for connection timeout, retry count, and retry delay.
|
|
51
51
|
* Falls back to sensible defaults if not provided.
|
|
52
52
|
*/
|
|
53
|
-
constructor(
|
|
54
|
-
this.
|
|
53
|
+
constructor(userId: string, options: MultiSessionOptions = {}) {
|
|
54
|
+
this.userId = userId;
|
|
55
55
|
this.options = {
|
|
56
56
|
timeout: DEFAULT_TIMEOUT_MS,
|
|
57
57
|
maxRetries: DEFAULT_MAX_RETRIES,
|
|
@@ -61,7 +61,7 @@ export class MultiSessionClient {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* Fetches all sessions for this
|
|
64
|
+
* Fetches all sessions for this userId from storage and returns only the
|
|
65
65
|
* ones that are ready to connect.
|
|
66
66
|
*
|
|
67
67
|
* A session is considered connectable when:
|
|
@@ -73,9 +73,9 @@ export class MultiSessionClient {
|
|
|
73
73
|
* Note: Sessions where `active` is `undefined` (legacy records) are included
|
|
74
74
|
* for backwards compatibility.
|
|
75
75
|
*/
|
|
76
|
-
private async getActiveSessions(): Promise<
|
|
77
|
-
const
|
|
78
|
-
const valid =
|
|
76
|
+
private async getActiveSessions(): Promise<Session[]> {
|
|
77
|
+
const sessionList = await sessions.list(this.userId);
|
|
78
|
+
const valid = sessionList.filter(s =>
|
|
79
79
|
s.serverId &&
|
|
80
80
|
s.serverUrl &&
|
|
81
81
|
s.callbackUrl &&
|
|
@@ -91,7 +91,7 @@ export class MultiSessionClient {
|
|
|
91
91
|
* has many active MCP sessions (e.g. 20+ servers). Within each batch, sessions
|
|
92
92
|
* are connected concurrently using `Promise.all` for speed.
|
|
93
93
|
*/
|
|
94
|
-
private async connectInBatches(sessions:
|
|
94
|
+
private async connectInBatches(sessions: Session[]): Promise<void> {
|
|
95
95
|
for (let i = 0; i < sessions.length; i += CONNECTION_BATCH_SIZE) {
|
|
96
96
|
const batch = sessions.slice(i, i + CONNECTION_BATCH_SIZE);
|
|
97
97
|
await Promise.all(batch.map(session => this.connectSession(session)));
|
|
@@ -110,7 +110,7 @@ export class MultiSessionClient {
|
|
|
110
110
|
* per-session mutex so we never spin up two physical connections for the same session.
|
|
111
111
|
* - On completion (success or failure), the promise is cleaned up from the map.
|
|
112
112
|
*/
|
|
113
|
-
private async connectSession(session:
|
|
113
|
+
private async connectSession(session: Session): Promise<void> {
|
|
114
114
|
const existingClient = this.clients.find(c => c.getSessionId() === session.sessionId);
|
|
115
115
|
if (existingClient?.isConnected()) {
|
|
116
116
|
return;
|
|
@@ -146,7 +146,7 @@ export class MultiSessionClient {
|
|
|
146
146
|
* If all attempts are exhausted, logs an error and returns silently (does not throw),
|
|
147
147
|
* so a single bad server doesn't block the rest of the batch from connecting.
|
|
148
148
|
*/
|
|
149
|
-
private async establishConnectionWithRetries(session:
|
|
149
|
+
private async establishConnectionWithRetries(session: Session): Promise<void> {
|
|
150
150
|
const maxRetries = this.options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
151
151
|
const retryDelay = this.options.retryDelay ?? DEFAULT_RETRY_DELAY_MS;
|
|
152
152
|
let lastError: unknown;
|
|
@@ -154,7 +154,7 @@ export class MultiSessionClient {
|
|
|
154
154
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
155
155
|
try {
|
|
156
156
|
const client = new MCPClient({
|
|
157
|
-
|
|
157
|
+
userId: this.userId,
|
|
158
158
|
sessionId: session.sessionId,
|
|
159
159
|
serverId: session.serverId,
|
|
160
160
|
serverUrl: session.serverUrl,
|
|
@@ -192,7 +192,7 @@ export class MultiSessionClient {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
/**
|
|
195
|
-
* The main entry point. Fetches all active sessions for this
|
|
195
|
+
* The main entry point. Fetches all active sessions for this userId from
|
|
196
196
|
* storage and establishes connections to all of them in batches.
|
|
197
197
|
*
|
|
198
198
|
* Call this once after creating the client. On traditional servers, you can
|
|
@@ -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
|
})
|