@mcp-ts/sdk 2.4.1 → 2.4.3
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/dist/adapters/agui-adapter.d.mts +3 -4
- package/dist/adapters/agui-adapter.d.ts +3 -4
- package/dist/adapters/agui-middleware.d.mts +3 -4
- package/dist/adapters/agui-middleware.d.ts +3 -4
- package/dist/adapters/ai-adapter.d.mts +3 -4
- package/dist/adapters/ai-adapter.d.ts +3 -4
- package/dist/adapters/langchain-adapter.d.mts +3 -4
- package/dist/adapters/langchain-adapter.d.ts +3 -4
- package/dist/adapters/mastra-adapter.d.mts +2 -2
- package/dist/adapters/mastra-adapter.d.ts +2 -2
- package/dist/bin/mcp-ts.js +5 -5
- package/dist/bin/mcp-ts.js.map +1 -1
- package/dist/bin/mcp-ts.mjs +5 -5
- package/dist/bin/mcp-ts.mjs.map +1 -1
- package/dist/client/index.d.mts +2 -3
- package/dist/client/index.d.ts +2 -3
- package/dist/client/react.d.mts +4 -6
- package/dist/client/react.d.ts +4 -6
- package/dist/client/vue.d.mts +4 -6
- package/dist/client/vue.d.ts +4 -6
- package/dist/{index-Ch7ouNSa.d.ts → index-B8kJSrBJ.d.ts} +1 -2
- package/dist/{index-L5XoXgsb.d.mts → index-DiJsm_lK.d.mts} +1 -2
- package/dist/index.d.mts +5 -6
- package/dist/index.d.ts +5 -6
- package/dist/index.js +174 -110
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +174 -110
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-k-9RvWvi.d.mts → multi-session-client-B6hsYO8V.d.mts} +218 -71
- package/dist/{multi-session-client-Bwm-efqg.d.ts → multi-session-client-DOBvtaQV.d.ts} +218 -71
- package/dist/server/index.d.mts +5 -128
- package/dist/server/index.d.ts +5 -128
- package/dist/server/index.js +174 -110
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +174 -110
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +4 -5
- package/dist/shared/index.d.ts +4 -5
- package/dist/{tool-router-CuApsDiV.d.mts → tool-router-CbG4Tum6.d.mts} +1 -1
- package/dist/{tool-router-CZMrOG8J.d.ts → tool-router-ChIhPwgP.d.ts} +1 -1
- package/dist/{types-DCk_IF4L.d.mts → types-CjczQwNX.d.mts} +122 -1
- package/dist/{types-DCk_IF4L.d.ts → types-CjczQwNX.d.ts} +122 -1
- package/package.json +1 -1
- package/src/bin/mcp-ts.ts +5 -5
- package/src/server/mcp/multi-session-client.ts +190 -132
- package/src/server/mcp/oauth-client.ts +49 -7
- package/src/server/storage/index.ts +8 -8
- package/dist/events-C4m7tK1U.d.mts +0 -122
- package/dist/events-C4m7tK1U.d.ts +0 -122
|
@@ -1,54 +1,90 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
1
|
import { MCPClient } from './oauth-client.js';
|
|
4
2
|
import { sessions, type Session } from '../storage/index.js';
|
|
3
|
+
import type { ToolClientProvider } from '../../shared/types.js';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Constants
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
5
8
|
|
|
6
9
|
const DEFAULT_TIMEOUT_MS = 15000;
|
|
7
10
|
const DEFAULT_MAX_RETRIES = 2;
|
|
8
11
|
const DEFAULT_RETRY_DELAY_MS = 1000;
|
|
9
12
|
const CONNECTION_BATCH_SIZE = 5;
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Types
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
15
18
|
export interface MultiSessionOptions {
|
|
16
19
|
/**
|
|
17
|
-
* Connection timeout in milliseconds
|
|
20
|
+
* Connection timeout in milliseconds.
|
|
18
21
|
* @default 15000
|
|
19
22
|
*/
|
|
20
23
|
timeout?: number;
|
|
24
|
+
|
|
21
25
|
/**
|
|
22
|
-
* Maximum number of retry attempts
|
|
26
|
+
* Maximum number of retry attempts per session.
|
|
23
27
|
* @default 2
|
|
24
28
|
*/
|
|
25
29
|
maxRetries?: number;
|
|
30
|
+
|
|
26
31
|
/**
|
|
27
|
-
* Delay between
|
|
32
|
+
* Delay between retry attempts in milliseconds.
|
|
28
33
|
* @default 1000
|
|
29
34
|
*/
|
|
30
35
|
retryDelay?: number;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Custom session provider. When provided, `connect()` calls this
|
|
39
|
+
* instead of querying the storage backend. Useful when sessions are
|
|
40
|
+
* synced externally (e.g. via Supabase Realtime, WebSocket, or an
|
|
41
|
+
* in-memory cache).
|
|
42
|
+
*
|
|
43
|
+
* Default: reads from the storage backend via `sessions.list(userId)`.
|
|
44
|
+
*/
|
|
45
|
+
sessionProvider?: () => Promise<Session[]>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Called after a session is successfully connected.
|
|
49
|
+
*/
|
|
50
|
+
onSessionConnected?: (sessionId: string, client: MCPClient) => void;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Called when a session is evicted from the in-memory client list
|
|
54
|
+
* because it no longer exists in the active sessions list.
|
|
55
|
+
*/
|
|
56
|
+
onSessionEvicted?: (sessionId: string) => void;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Called when all retry attempts for a session have been exhausted.
|
|
60
|
+
*/
|
|
61
|
+
onSessionFailed?: (sessionId: string, error: unknown) => void;
|
|
31
62
|
}
|
|
32
63
|
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// MultiSessionClient
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
33
68
|
/**
|
|
34
69
|
* Manages multiple MCP client connections for a single user.
|
|
35
70
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
71
|
+
* Cached instances stay alive between requests on long-running servers.
|
|
72
|
+
* On serverless, the underlying session data persists in storage — the
|
|
73
|
+
* client rehydrates from `sessionProvider` (or the default storage backend)
|
|
74
|
+
* each time `connect()` is called.
|
|
75
|
+
*
|
|
76
|
+
* @implements {ToolClientProvider}
|
|
40
77
|
*/
|
|
41
|
-
export class MultiSessionClient {
|
|
78
|
+
export class MultiSessionClient implements ToolClientProvider {
|
|
42
79
|
private clients: MCPClient[] = [];
|
|
80
|
+
private connectionPromises = new Map<string, Promise<void>>();
|
|
43
81
|
private userId: string;
|
|
44
|
-
private options: MultiSessionOptions
|
|
82
|
+
private options: Required<Pick<MultiSessionOptions, 'timeout' | 'maxRetries' | 'retryDelay'>> &
|
|
83
|
+
Pick<MultiSessionOptions, 'sessionProvider' | 'onSessionConnected' | 'onSessionEvicted' | 'onSessionFailed'>;
|
|
45
84
|
|
|
46
85
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* @param userId - A unique string identifying the user (e.g. user ID or email).
|
|
50
|
-
* @param options - Optional tuning for connection timeout, retry count, and retry delay.
|
|
51
|
-
* Falls back to sensible defaults if not provided.
|
|
86
|
+
* @param userId - Unique identifier for the user (e.g. user ID or email).
|
|
87
|
+
* @param options - Optional tuning and lifecycle hooks.
|
|
52
88
|
*/
|
|
53
89
|
constructor(userId: string, options: MultiSessionOptions = {}) {
|
|
54
90
|
this.userId = userId;
|
|
@@ -56,36 +92,123 @@ export class MultiSessionClient {
|
|
|
56
92
|
timeout: DEFAULT_TIMEOUT_MS,
|
|
57
93
|
maxRetries: DEFAULT_MAX_RETRIES,
|
|
58
94
|
retryDelay: DEFAULT_RETRY_DELAY_MS,
|
|
59
|
-
...options
|
|
95
|
+
...options,
|
|
60
96
|
};
|
|
61
97
|
}
|
|
62
98
|
|
|
99
|
+
// -----------------------------------------------------------------------
|
|
100
|
+
// Public API
|
|
101
|
+
// -----------------------------------------------------------------------
|
|
102
|
+
|
|
63
103
|
/**
|
|
64
|
-
* Fetches
|
|
65
|
-
* ones that are ready to connect.
|
|
104
|
+
* Fetches active sessions and establishes connections to all of them.
|
|
66
105
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* and let the OAuth flow complete separately before we try to reconnect them.
|
|
106
|
+
* Call this once after creating the client. On long-running servers you
|
|
107
|
+
* can cache the `MultiSessionClient` instance and call `connect()` on
|
|
108
|
+
* each request — already-connected sessions are skipped internally.
|
|
71
109
|
*/
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
const
|
|
110
|
+
async connect(): Promise<void> {
|
|
111
|
+
const sessions = await this.fetchActiveSessions();
|
|
112
|
+
const activeSessionIds = new Set(sessions.map(s => s.sessionId));
|
|
113
|
+
|
|
114
|
+
// Evict clients whose sessions no longer appear in the active list.
|
|
115
|
+
// Without this, a stale MCPClient with a live SDK transport would
|
|
116
|
+
// stay in the clients array after its session was deleted externally
|
|
117
|
+
// (e.g. via the SSE handler's clearSession()), causing OAuth refreshes
|
|
118
|
+
// to fail with a FK violation when they try to patchCredentials()
|
|
119
|
+
// against the now-deleted session.
|
|
120
|
+
for (const client of this.clients) {
|
|
121
|
+
if (!activeSessionIds.has(client.getSessionId())) {
|
|
122
|
+
this.options.onSessionEvicted?.(client.getSessionId());
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
this.clients = this.clients.filter(c => activeSessionIds.has(c.getSessionId()));
|
|
126
|
+
|
|
127
|
+
await this.connectInBatches(sessions);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Drops all cached `MCPClient` instances and reconnects fresh from storage.
|
|
132
|
+
*
|
|
133
|
+
* Call this when downstream MCP servers have expired their transport sessions
|
|
134
|
+
* (e.g. after a remote server restart) and subsequent tool calls return
|
|
135
|
+
* "Session not found. Reconnect without session header." errors.
|
|
136
|
+
*
|
|
137
|
+
* OAuth tokens are preserved in the storage backend — no re-authentication
|
|
138
|
+
* is required. Only the in-memory transport sessions are cleared.
|
|
139
|
+
*/
|
|
140
|
+
async reconnect(): Promise<void> {
|
|
141
|
+
await this.disconnect();
|
|
142
|
+
await this.connect();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Returns all currently connected `MCPClient` instances.
|
|
147
|
+
*
|
|
148
|
+
* Use this to enumerate available tools across all connected servers,
|
|
149
|
+
* or to route a tool call to the right client by `serverId`.
|
|
150
|
+
*/
|
|
151
|
+
getClients(): MCPClient[] {
|
|
152
|
+
return this.clients;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Removes and disconnects a single session by ID.
|
|
157
|
+
*
|
|
158
|
+
* @returns `true` if the session was found and removed, `false` if not found.
|
|
159
|
+
*/
|
|
160
|
+
async removeSession(sessionId: string): Promise<boolean> {
|
|
161
|
+
const idx = this.clients.findIndex(c => c.getSessionId() === sessionId);
|
|
162
|
+
if (idx === -1) return false;
|
|
163
|
+
const [client] = this.clients.splice(idx, 1);
|
|
164
|
+
await client.disconnect();
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Gracefully disconnects all active MCP clients and clears the internal list.
|
|
170
|
+
*
|
|
171
|
+
* For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
|
|
172
|
+
* endpoint per the spec before closing locally. All disconnects run in
|
|
173
|
+
* parallel so shutdown is not serialised across many sessions.
|
|
174
|
+
*
|
|
175
|
+
* Call this during server shutdown or when a user logs out to free up
|
|
176
|
+
* underlying transport resources (SSE streams, HTTP connections, etc.).
|
|
177
|
+
*/
|
|
178
|
+
async disconnect(): Promise<void> {
|
|
179
|
+
await Promise.all(this.clients.map((client) => client.disconnect()));
|
|
180
|
+
this.clients = [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// -----------------------------------------------------------------------
|
|
184
|
+
// Internals
|
|
185
|
+
// -----------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolves the list of sessions to connect.
|
|
189
|
+
*
|
|
190
|
+
* Uses the custom `sessionProvider` when provided, otherwise falls back
|
|
191
|
+
* to querying the storage backend via `sessions.list(userId)`.
|
|
192
|
+
*/
|
|
193
|
+
private async fetchActiveSessions(): Promise<Session[]> {
|
|
194
|
+
const sessionList = this.options.sessionProvider
|
|
195
|
+
? await this.options.sessionProvider()
|
|
196
|
+
: await sessions.list(this.userId);
|
|
197
|
+
|
|
198
|
+
return sessionList.filter(s =>
|
|
75
199
|
s.serverId &&
|
|
76
200
|
s.serverUrl &&
|
|
77
201
|
s.callbackUrl &&
|
|
78
202
|
s.status === 'active'
|
|
79
203
|
);
|
|
80
|
-
return valid;
|
|
81
204
|
}
|
|
82
205
|
|
|
83
206
|
/**
|
|
84
|
-
* Connects
|
|
207
|
+
* Connects a list of sessions in controlled batches.
|
|
85
208
|
*
|
|
86
|
-
* Batching prevents overwhelming the event loop or external servers when
|
|
87
|
-
* has many active MCP sessions
|
|
88
|
-
*
|
|
209
|
+
* Batching prevents overwhelming the event loop or external servers when
|
|
210
|
+
* a user has many active MCP sessions. Within each batch, sessions are
|
|
211
|
+
* connected concurrently using `Promise.all`.
|
|
89
212
|
*/
|
|
90
213
|
private async connectInBatches(sessions: Session[]): Promise<void> {
|
|
91
214
|
for (let i = 0; i < sessions.length; i += CONNECTION_BATCH_SIZE) {
|
|
@@ -94,38 +217,30 @@ export class MultiSessionClient {
|
|
|
94
217
|
}
|
|
95
218
|
}
|
|
96
219
|
|
|
97
|
-
private connectionPromises = new Map<string, Promise<void>>();
|
|
98
|
-
|
|
99
220
|
/**
|
|
100
|
-
* Connects a single session, with
|
|
221
|
+
* Connects a single session, with deduplication to prevent race conditions.
|
|
101
222
|
*
|
|
102
|
-
* - If a client for this session already exists and is connected, returns
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* - If a connection attempt for this session is already in-flight
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* - On completion (success or failure), the promise is cleaned up from the map.
|
|
223
|
+
* - If a client for this session already exists and is connected, returns
|
|
224
|
+
* immediately.
|
|
225
|
+
* - If the existing client entry is no longer connected (e.g. explicit
|
|
226
|
+
* disconnect), it is evicted so a fresh transport is created.
|
|
227
|
+
* - If a connection attempt for this session is already in-flight, the
|
|
228
|
+
* existing promise is reused as a per-session mutex.
|
|
229
|
+
* - On completion (success or failure), the promise is cleaned up from
|
|
230
|
+
* the connectionPromises map.
|
|
111
231
|
*/
|
|
112
232
|
private async connectSession(session: Session): Promise<void> {
|
|
113
233
|
const existing = this.clients.find(c => c.getSessionId() === session.sessionId);
|
|
114
234
|
|
|
115
235
|
if (existing) {
|
|
116
236
|
if (existing.isConnected()) {
|
|
117
|
-
// Genuinely connected — nothing to do.
|
|
118
237
|
return;
|
|
119
238
|
}
|
|
120
239
|
|
|
121
|
-
|
|
122
|
-
// a prior failed reconnect attempt). Remove it so the fresh connect below
|
|
123
|
-
// starts with a clean slate and the underlying SDK Client doesn't complain
|
|
124
|
-
// about an already-attached transport.
|
|
240
|
+
this.options.onSessionEvicted?.(existing.getSessionId());
|
|
125
241
|
this.clients = this.clients.filter(c => c !== existing);
|
|
126
242
|
}
|
|
127
243
|
|
|
128
|
-
// Avoid concurrent connection attempts for the same session
|
|
129
244
|
if (this.connectionPromises.has(session.sessionId)) {
|
|
130
245
|
return this.connectionPromises.get(session.sessionId)!;
|
|
131
246
|
}
|
|
@@ -142,22 +257,19 @@ export class MultiSessionClient {
|
|
|
142
257
|
}
|
|
143
258
|
|
|
144
259
|
/**
|
|
145
|
-
*
|
|
260
|
+
* Core connection loop for a single session with retry logic.
|
|
146
261
|
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* 2. Races the connect call against a timeout promise — if the server doesn't respond
|
|
151
|
-
* within `timeoutMs`, the attempt is aborted and counted as a failure.
|
|
152
|
-
* 3. On success, replaces any stale client entry for this session in the `clients` array.
|
|
262
|
+
* 1. Creates a fresh `MCPClient` from the session data.
|
|
263
|
+
* 2. Races `client.connect()` against a timeout.
|
|
264
|
+
* 3. On success, replaces any stale entry and fires `onSessionConnected`.
|
|
153
265
|
* 4. On failure, waits `retryDelay` ms before the next attempt.
|
|
154
266
|
*
|
|
155
|
-
* If all attempts are exhausted, logs an error and returns silently
|
|
156
|
-
*
|
|
267
|
+
* If all attempts are exhausted, logs an error and returns silently so
|
|
268
|
+
* a single bad server doesn't block the rest of the batch.
|
|
157
269
|
*/
|
|
158
270
|
private async establishConnectionWithRetries(session: Session): Promise<void> {
|
|
159
|
-
const maxRetries = this.options.maxRetries
|
|
160
|
-
const retryDelay = this.options.retryDelay
|
|
271
|
+
const maxRetries = this.options.maxRetries;
|
|
272
|
+
const retryDelay = this.options.retryDelay;
|
|
161
273
|
let lastError: unknown;
|
|
162
274
|
|
|
163
275
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
@@ -173,10 +285,13 @@ export class MultiSessionClient {
|
|
|
173
285
|
headers: session.headers,
|
|
174
286
|
});
|
|
175
287
|
|
|
176
|
-
const timeoutMs = this.options.timeout
|
|
288
|
+
const timeoutMs = this.options.timeout;
|
|
177
289
|
let timeoutTimer: ReturnType<typeof setTimeout>;
|
|
178
290
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
179
|
-
timeoutTimer = setTimeout(
|
|
291
|
+
timeoutTimer = setTimeout(
|
|
292
|
+
() => reject(new Error(`Connection timed out after ${timeoutMs}ms`)),
|
|
293
|
+
timeoutMs,
|
|
294
|
+
);
|
|
180
295
|
});
|
|
181
296
|
|
|
182
297
|
try {
|
|
@@ -185,10 +300,11 @@ export class MultiSessionClient {
|
|
|
185
300
|
clearTimeout(timeoutTimer!);
|
|
186
301
|
}
|
|
187
302
|
|
|
188
|
-
// Always replace the disconnected client entry
|
|
189
303
|
this.clients = this.clients.filter(c => c.getSessionId() !== session.sessionId);
|
|
190
304
|
this.clients.push(client);
|
|
191
|
-
|
|
305
|
+
this.options.onSessionConnected?.(session.sessionId, client);
|
|
306
|
+
return;
|
|
307
|
+
|
|
192
308
|
} catch (error) {
|
|
193
309
|
lastError = error;
|
|
194
310
|
if (attempt < maxRetries) {
|
|
@@ -197,69 +313,11 @@ export class MultiSessionClient {
|
|
|
197
313
|
}
|
|
198
314
|
}
|
|
199
315
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
*
|
|
207
|
-
* Call this once after creating the client. On traditional servers, you can
|
|
208
|
-
* cache the `MultiSessionClient` instance after calling `connect()` to avoid
|
|
209
|
-
* re-fetching and re-connecting on every request.
|
|
210
|
-
*/
|
|
211
|
-
async connect(): Promise<void> {
|
|
212
|
-
const sessions = await this.getActiveSessions();
|
|
213
|
-
const activeSessionIds = new Set(sessions.map(s => s.sessionId));
|
|
214
|
-
|
|
215
|
-
// Prune stale clients whose sessions no longer exist in storage.
|
|
216
|
-
// Otherwise a lingering MCPClient with a still-alive SDK Client
|
|
217
|
-
// transport triggers OAuth refreshes on listTools(), and the
|
|
218
|
-
// StorageOAuthClientProvider.state() -> patchCredentials() call
|
|
219
|
-
// fails with a FK violation because the session was deleted.
|
|
220
|
-
this.clients = this.clients.filter(c => activeSessionIds.has(c.getSessionId()));
|
|
221
|
-
|
|
222
|
-
await this.connectInBatches(sessions);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Drops all cached `MCPClient` instances and reconnects fresh from storage.
|
|
227
|
-
*
|
|
228
|
-
* Call this when downstream MCP servers have expired their transport sessions
|
|
229
|
-
* (e.g. after a remote server restart) and subsequent tool calls return
|
|
230
|
-
* "Session not found. Reconnect without session header." errors.
|
|
231
|
-
*
|
|
232
|
-
* OAuth tokens are preserved in the storage backend — no re-authentication
|
|
233
|
-
* is required. Only the in-memory transport sessions are cleared.
|
|
234
|
-
*/
|
|
235
|
-
async reconnect(): Promise<void> {
|
|
236
|
-
await this.disconnect();
|
|
237
|
-
await this.connect();
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Returns all currently connected `MCPClient` instances.
|
|
242
|
-
*
|
|
243
|
-
* Use this to enumerate available tools across all connected servers,
|
|
244
|
-
* or to route a tool call to the right client by `serverId`.
|
|
245
|
-
*/
|
|
246
|
-
getClients(): MCPClient[] {
|
|
247
|
-
return this.clients;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Gracefully disconnects all active MCP clients and clears the internal client list.
|
|
252
|
-
*
|
|
253
|
-
* For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
|
|
254
|
-
* endpoint per the spec before closing locally. All disconnects run in
|
|
255
|
-
* parallel so shutdown is not serialised across many sessions.
|
|
256
|
-
*
|
|
257
|
-
* Call this during server shutdown or when a user logs out to free up
|
|
258
|
-
* underlying transport resources (SSE streams, HTTP connections, etc.).
|
|
259
|
-
*/
|
|
260
|
-
async disconnect(): Promise<void> {
|
|
261
|
-
await Promise.all(this.clients.map((client) => client.disconnect()));
|
|
262
|
-
this.clients = [];
|
|
316
|
+
this.options.onSessionFailed?.(session.sessionId, lastError);
|
|
317
|
+
console.error(
|
|
318
|
+
`[MultiSessionClient] Failed to connect to session ${session.sessionId} ` +
|
|
319
|
+
`after ${maxRetries + 1} attempts:`,
|
|
320
|
+
lastError,
|
|
321
|
+
);
|
|
263
322
|
}
|
|
264
323
|
}
|
|
265
|
-
|
|
@@ -257,7 +257,6 @@ export class MCPClient {
|
|
|
257
257
|
const hasSessionHeader = init?.headers && new Headers(init.headers as HeadersInit).has('mcp-session-id');
|
|
258
258
|
|
|
259
259
|
if (response.status === 404 && hasSessionHeader) {
|
|
260
|
-
this.client = null;
|
|
261
260
|
throw new Error("MCP_SESSION_EXPIRED: Downstream session was not found on the server.");
|
|
262
261
|
}
|
|
263
262
|
|
|
@@ -791,7 +790,9 @@ export class MCPClient {
|
|
|
791
790
|
params: {},
|
|
792
791
|
};
|
|
793
792
|
|
|
794
|
-
const result = await this.
|
|
793
|
+
const result = await this.withRetry(() =>
|
|
794
|
+
this.client!.request(request, ListToolsResultSchema)
|
|
795
|
+
);
|
|
795
796
|
|
|
796
797
|
if (this.serverId) {
|
|
797
798
|
this._onConnectionEvent.fire({
|
|
@@ -837,7 +838,9 @@ export class MCPClient {
|
|
|
837
838
|
};
|
|
838
839
|
|
|
839
840
|
try {
|
|
840
|
-
const result = await this.
|
|
841
|
+
const result = await this.withRetry(() =>
|
|
842
|
+
this.client!.request(request, CallToolResultSchema)
|
|
843
|
+
);
|
|
841
844
|
|
|
842
845
|
this._onObservabilityEvent.fire({
|
|
843
846
|
type: 'mcp:client:tool_call',
|
|
@@ -897,7 +900,9 @@ export class MCPClient {
|
|
|
897
900
|
params: {},
|
|
898
901
|
};
|
|
899
902
|
|
|
900
|
-
const result = await this.
|
|
903
|
+
const result = await this.withRetry(() =>
|
|
904
|
+
this.client!.request(request, ListPromptsResultSchema)
|
|
905
|
+
);
|
|
901
906
|
|
|
902
907
|
this.emitStateChange('READY');
|
|
903
908
|
this.emitProgress(`Discovered ${result.prompts.length} prompts`);
|
|
@@ -931,7 +936,9 @@ export class MCPClient {
|
|
|
931
936
|
},
|
|
932
937
|
};
|
|
933
938
|
|
|
934
|
-
return await this.
|
|
939
|
+
return await this.withRetry(() =>
|
|
940
|
+
this.client!.request(request, GetPromptResultSchema)
|
|
941
|
+
);
|
|
935
942
|
}
|
|
936
943
|
|
|
937
944
|
/**
|
|
@@ -952,7 +959,9 @@ export class MCPClient {
|
|
|
952
959
|
params: {},
|
|
953
960
|
};
|
|
954
961
|
|
|
955
|
-
const result = await this.
|
|
962
|
+
const result = await this.withRetry(() =>
|
|
963
|
+
this.client!.request(request, ListResourcesResultSchema)
|
|
964
|
+
);
|
|
956
965
|
|
|
957
966
|
this.emitStateChange('READY');
|
|
958
967
|
this.emitProgress(`Discovered ${result.resources.length} resources`);
|
|
@@ -984,7 +993,34 @@ export class MCPClient {
|
|
|
984
993
|
},
|
|
985
994
|
};
|
|
986
995
|
|
|
987
|
-
return await this.
|
|
996
|
+
return await this.withRetry(() =>
|
|
997
|
+
this.client!.request(request, ReadResourceResultSchema)
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Wraps an MCP request with automatic transport-session recovery.
|
|
1003
|
+
*
|
|
1004
|
+
* When the downstream MCP server rejects the request with a 404 indicating
|
|
1005
|
+
* the transport session has expired, this method tears down the stale SDK
|
|
1006
|
+
* client and transport, calls {@link reconnect} to negotiate a fresh session,
|
|
1007
|
+
* and retries the request once.
|
|
1008
|
+
*
|
|
1009
|
+
* Non-transient errors (network failures, auth errors, etc.) propagate as-is.
|
|
1010
|
+
*/
|
|
1011
|
+
private async withRetry<T>(fn: () => Promise<T>): Promise<T> {
|
|
1012
|
+
try {
|
|
1013
|
+
return await fn();
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
if (!(error instanceof Error && error.message.includes('MCP_SESSION_EXPIRED'))) throw error;
|
|
1016
|
+
if (this.client) {
|
|
1017
|
+
try { await this.client.close(); } catch {}
|
|
1018
|
+
this.transport = null;
|
|
1019
|
+
this.client = null;
|
|
1020
|
+
}
|
|
1021
|
+
await this.reconnect();
|
|
1022
|
+
return await fn();
|
|
1023
|
+
}
|
|
988
1024
|
}
|
|
989
1025
|
|
|
990
1026
|
/**
|
|
@@ -1000,6 +1036,12 @@ export class MCPClient {
|
|
|
1000
1036
|
throw new Error('OAuth provider not initialized');
|
|
1001
1037
|
}
|
|
1002
1038
|
|
|
1039
|
+
// Close the client initialize() may have created — we need a fresh
|
|
1040
|
+
// client that will negotiate a new transport session.
|
|
1041
|
+
if (this.client) {
|
|
1042
|
+
try { await this.client.close(); } catch {}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1003
1045
|
this.client = new Client(
|
|
1004
1046
|
{
|
|
1005
1047
|
name: MCP_CLIENT_NAME,
|
|
@@ -141,13 +141,13 @@ async function createStorage(): Promise<SessionStore> {
|
|
|
141
141
|
|
|
142
142
|
if (type === 'supabase') {
|
|
143
143
|
const url = process.env.SUPABASE_URL;
|
|
144
|
-
const key = process.env.
|
|
144
|
+
const key = process.env.SUPABASE_SECRET_KEY || process.env.SUPABASE_ANON_KEY;
|
|
145
145
|
|
|
146
146
|
if (!url || !key) {
|
|
147
|
-
console.warn('[mcp-ts][Storage] Explicit selection "supabase" requires SUPABASE_URL and
|
|
147
|
+
console.warn('[mcp-ts][Storage] Explicit selection "supabase" requires SUPABASE_URL and SUPABASE_SECRET_KEY.');
|
|
148
148
|
} else {
|
|
149
|
-
if (!process.env.
|
|
150
|
-
console.warn('[mcp-ts][Storage] ⚠️ Warning: Using "SUPABASE_ANON_KEY" for server-side storage. You may encounter RLS policy violations. "
|
|
149
|
+
if (!process.env.SUPABASE_SECRET_KEY) {
|
|
150
|
+
console.warn('[mcp-ts][Storage] ⚠️ Warning: Using "SUPABASE_ANON_KEY" for server-side storage. You may encounter RLS policy violations. "SUPABASE_SECRET_KEY" is recommended.');
|
|
151
151
|
}
|
|
152
152
|
try {
|
|
153
153
|
const { createClient } = await import('@supabase/supabase-js');
|
|
@@ -210,14 +210,14 @@ async function createStorage(): Promise<SessionStore> {
|
|
|
210
210
|
return await initializeStorage(new SqliteStorage({ path: process.env.MCP_TS_STORAGE_SQLITE_PATH }));
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
if (process.env.SUPABASE_URL && (process.env.
|
|
213
|
+
if (process.env.SUPABASE_URL && (process.env.SUPABASE_SECRET_KEY || process.env.SUPABASE_ANON_KEY)) {
|
|
214
214
|
try {
|
|
215
215
|
const { createClient } = await import('@supabase/supabase-js');
|
|
216
216
|
const url = process.env.SUPABASE_URL;
|
|
217
|
-
const key = process.env.
|
|
217
|
+
const key = process.env.SUPABASE_SECRET_KEY || process.env.SUPABASE_ANON_KEY!;
|
|
218
218
|
|
|
219
|
-
if (!process.env.
|
|
220
|
-
console.warn('[mcp-ts][Storage] ⚠️ Warning: Using "SUPABASE_ANON_KEY" for server-side storage. You may encounter RLS policy violations. "
|
|
219
|
+
if (!process.env.SUPABASE_SECRET_KEY) {
|
|
220
|
+
console.warn('[mcp-ts][Storage] ⚠️ Warning: Using "SUPABASE_ANON_KEY" for server-side storage. You may encounter RLS policy violations. "SUPABASE_SECRET_KEY" is recommended.');
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
const client = createClient(url, key);
|