@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
|
@@ -25,7 +25,7 @@ export interface UseMcpOptions {
|
|
|
25
25
|
/**
|
|
26
26
|
* User/Client identifier
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
userId: string;
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Optional auth token
|
|
@@ -109,7 +109,7 @@ export interface McpClient {
|
|
|
109
109
|
serverName: string;
|
|
110
110
|
serverUrl: string;
|
|
111
111
|
callbackUrl: string;
|
|
112
|
-
transportType?: 'sse' | '
|
|
112
|
+
transportType?: 'sse' | 'streamable-http';
|
|
113
113
|
}) => Promise<string>;
|
|
114
114
|
|
|
115
115
|
/**
|
|
@@ -125,7 +125,7 @@ export interface McpClient {
|
|
|
125
125
|
serverName: string;
|
|
126
126
|
serverUrl: string;
|
|
127
127
|
callbackUrl: string;
|
|
128
|
-
transportType?: 'sse' | '
|
|
128
|
+
transportType?: 'sse' | 'streamable-http';
|
|
129
129
|
}) => Promise<string>;
|
|
130
130
|
|
|
131
131
|
/**
|
|
@@ -219,7 +219,7 @@ export interface McpClient {
|
|
|
219
219
|
export function useMcp(options: UseMcpOptions): McpClient {
|
|
220
220
|
const {
|
|
221
221
|
url,
|
|
222
|
-
|
|
222
|
+
userId,
|
|
223
223
|
authToken,
|
|
224
224
|
autoConnect = true,
|
|
225
225
|
autoInitialize = true,
|
|
@@ -386,7 +386,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
386
386
|
try {
|
|
387
387
|
isInitializing.value = true;
|
|
388
388
|
|
|
389
|
-
const result = await clientRef.value.
|
|
389
|
+
const result = await clientRef.value.listSessions();
|
|
390
390
|
const sessions = result.sessions || [];
|
|
391
391
|
|
|
392
392
|
// Initialize connections
|
|
@@ -413,7 +413,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
413
413
|
return;
|
|
414
414
|
}
|
|
415
415
|
suppressAuthRedirectSessions.value.add(session.sessionId);
|
|
416
|
-
await clientRef.value.
|
|
416
|
+
await clientRef.value.getSession(session.sessionId);
|
|
417
417
|
} catch (error) {
|
|
418
418
|
console.error(`[useMcp] Failed to validate session ${session.sessionId}:`, error);
|
|
419
419
|
} finally {
|
|
@@ -443,7 +443,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
443
443
|
|
|
444
444
|
const clientOptions: SSEClientOptions = {
|
|
445
445
|
url,
|
|
446
|
-
|
|
446
|
+
userId,
|
|
447
447
|
authToken,
|
|
448
448
|
onConnectionEvent: (event) => {
|
|
449
449
|
// Update local state based on event
|
|
@@ -493,7 +493,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
493
493
|
serverName: string;
|
|
494
494
|
serverUrl: string;
|
|
495
495
|
callbackUrl: string;
|
|
496
|
-
transportType?: 'sse' | '
|
|
496
|
+
transportType?: 'sse' | 'streamable-http';
|
|
497
497
|
}): Promise<string> => {
|
|
498
498
|
if (!clientRef.value) {
|
|
499
499
|
throw new Error('SSE client not initialized');
|
|
@@ -511,7 +511,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
511
511
|
serverName: string;
|
|
512
512
|
serverUrl: string;
|
|
513
513
|
callbackUrl: string;
|
|
514
|
-
transportType?: 'sse' | '
|
|
514
|
+
transportType?: 'sse' | 'streamable-http';
|
|
515
515
|
}): Promise<string> => {
|
|
516
516
|
if (!clientRef.value) {
|
|
517
517
|
throw new Error('SSE client not initialized');
|
|
@@ -589,7 +589,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
589
589
|
throw new Error('SSE client not initialized');
|
|
590
590
|
}
|
|
591
591
|
suppressAuthRedirectSessions.value.delete(sessionId);
|
|
592
|
-
await clientRef.value.
|
|
592
|
+
await clientRef.value.getSession(sessionId);
|
|
593
593
|
};
|
|
594
594
|
|
|
595
595
|
/**
|
|
@@ -12,9 +12,9 @@ import type { McpRpcResponse } from '../../shared/types.js';
|
|
|
12
12
|
|
|
13
13
|
export interface NextMcpHandlerOptions {
|
|
14
14
|
/**
|
|
15
|
-
* Extract
|
|
15
|
+
* Extract userId from request (default: from 'userId' query param)
|
|
16
16
|
*/
|
|
17
|
-
|
|
17
|
+
getUserId?: (request: Request) => string | null;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Extract auth token from request (default: from 'token' query param or Authorization header)
|
|
@@ -25,7 +25,7 @@ export interface NextMcpHandlerOptions {
|
|
|
25
25
|
* Authenticate user and verify access (optional)
|
|
26
26
|
* Return true if user is authenticated, false otherwise
|
|
27
27
|
*/
|
|
28
|
-
authenticate?: (
|
|
28
|
+
authenticate?: (userId: string, token: string | null) => Promise<boolean> | boolean;
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Heartbeat interval in milliseconds (default: 30000)
|
|
@@ -45,10 +45,13 @@ export interface NextMcpHandlerOptions {
|
|
|
45
45
|
|
|
46
46
|
export function createNextMcpHandler(options: NextMcpHandlerOptions = {}) {
|
|
47
47
|
const {
|
|
48
|
-
|
|
48
|
+
getUserId = (request: Request) => request.headers.get('x-mcp-user-id'),
|
|
49
49
|
getAuthToken = (request: Request) => {
|
|
50
|
-
const
|
|
51
|
-
|
|
50
|
+
const authHeader = request.headers.get('authorization');
|
|
51
|
+
if (authHeader?.toLowerCase().startsWith('bearer ')) {
|
|
52
|
+
return authHeader.slice(7);
|
|
53
|
+
}
|
|
54
|
+
return authHeader;
|
|
52
55
|
},
|
|
53
56
|
authenticate = () => true,
|
|
54
57
|
heartbeatInterval = 30000,
|
|
@@ -56,8 +59,8 @@ export function createNextMcpHandler(options: NextMcpHandlerOptions = {}) {
|
|
|
56
59
|
getClientMetadata,
|
|
57
60
|
} = options;
|
|
58
61
|
|
|
59
|
-
const toManagerOptions = (
|
|
60
|
-
|
|
62
|
+
const toManagerOptions = (userId: string, resolvedClientMetadata?: ClientMetadata) => ({
|
|
63
|
+
userId,
|
|
61
64
|
heartbeatInterval,
|
|
62
65
|
clientDefaults: resolvedClientMetadata,
|
|
63
66
|
});
|
|
@@ -79,15 +82,15 @@ export function createNextMcpHandler(options: NextMcpHandlerOptions = {}) {
|
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
async function POST(request: Request): Promise<Response> {
|
|
82
|
-
const
|
|
85
|
+
const userId = getUserId(request);
|
|
83
86
|
const authToken = getAuthToken(request);
|
|
84
87
|
const acceptsEventStream = (request.headers.get('accept') || '').toLowerCase().includes('text/event-stream');
|
|
85
88
|
|
|
86
|
-
if (!
|
|
87
|
-
return Response.json({ error: { code: '
|
|
89
|
+
if (!userId) {
|
|
90
|
+
return Response.json({ error: { code: 'MISSING_userId', message: 'Missing userId' } }, { status: 400 });
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
const isAuthorized = await authenticate(
|
|
93
|
+
const isAuthorized = await authenticate(userId, authToken);
|
|
91
94
|
if (!isAuthorized) {
|
|
92
95
|
return Response.json({ error: { code: 'UNAUTHORIZED', message: 'Unauthorized' } }, { status: 401 });
|
|
93
96
|
}
|
|
@@ -113,7 +116,7 @@ export function createNextMcpHandler(options: NextMcpHandlerOptions = {}) {
|
|
|
113
116
|
|
|
114
117
|
if (!acceptsEventStream) {
|
|
115
118
|
const manager = new SSEConnectionManager(
|
|
116
|
-
toManagerOptions(
|
|
119
|
+
toManagerOptions(userId, resolvedClientMetadata),
|
|
117
120
|
() => { }
|
|
118
121
|
);
|
|
119
122
|
try {
|
|
@@ -138,7 +141,7 @@ export function createNextMcpHandler(options: NextMcpHandlerOptions = {}) {
|
|
|
138
141
|
};
|
|
139
142
|
|
|
140
143
|
const manager = new SSEConnectionManager(
|
|
141
|
-
toManagerOptions(
|
|
144
|
+
toManagerOptions(userId, resolvedClientMetadata),
|
|
142
145
|
(event: McpConnectionEvent | McpObservabilityEvent | McpRpcResponse) => {
|
|
143
146
|
if (isRpcResponseEvent(event)) {
|
|
144
147
|
sendSSE('rpc-response', event);
|
|
@@ -183,7 +186,7 @@ export function createNextMcpHandler(options: NextMcpHandlerOptions = {}) {
|
|
|
183
186
|
} catch (error) {
|
|
184
187
|
const err = error instanceof Error ? error : new Error('Unknown error');
|
|
185
188
|
console.error('[MCP Next Handler] Failed to handle RPC', {
|
|
186
|
-
|
|
189
|
+
userId,
|
|
187
190
|
message: err.message,
|
|
188
191
|
stack: err.stack,
|
|
189
192
|
rawBody: rawBody.slice(0, 500),
|
|
@@ -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
|