@mcp-ts/sdk 2.3.4 → 2.4.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 +113 -30
- package/dist/adapters/agui-adapter.d.mts +3 -3
- package/dist/adapters/agui-adapter.d.ts +3 -3
- 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 +3 -3
- package/dist/adapters/ai-adapter.d.ts +3 -3
- package/dist/adapters/langchain-adapter.d.mts +3 -3
- package/dist/adapters/langchain-adapter.d.ts +3 -3
- package/dist/adapters/mastra-adapter.d.mts +1 -1
- package/dist/adapters/mastra-adapter.d.ts +1 -1
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +2 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +2 -2
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +9 -8
- package/dist/client/react.d.ts +9 -8
- package/dist/client/react.js +66 -26
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +67 -27
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +6 -5
- package/dist/client/vue.d.ts +6 -5
- package/dist/client/vue.js +27 -17
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +27 -17
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{index-Cfjsme-a.d.mts → index-ByIjEReo.d.mts} +2 -2
- package/dist/{index-CmjMd2ac.d.ts → index-CtXvKl8N.d.ts} +2 -2
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +728 -306
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +722 -304
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-C7hGqzgM.d.mts → multi-session-client-CIMUGF8S.d.mts} +15 -33
- package/dist/{multi-session-client-C_kPHpD5.d.ts → multi-session-client-CnvZEGPY.d.ts} +15 -33
- package/dist/server/index.d.mts +45 -17
- package/dist/server/index.d.ts +45 -17
- package/dist/server/index.js +720 -300
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +716 -299
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +9 -8
- package/dist/shared/index.d.ts +9 -8
- package/dist/shared/index.js +7 -5
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +5 -4
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{tool-router-BMzhoNYt.d.ts → tool-router-CZMrOG8J.d.ts} +1 -1
- package/dist/{tool-router-BhHsvBfP.d.mts → tool-router-CuApsDiV.d.mts} +1 -1
- package/dist/{types-CxFaaZrM.d.mts → types-DCk_IF4L.d.mts} +5 -3
- package/dist/{types-CxFaaZrM.d.ts → types-DCk_IF4L.d.ts} +5 -3
- package/migrations/neon/20260513010000_install_mcp_sessions.sql +40 -3
- package/migrations/neon/20260513020000_add_session_cleanup_cron.sql +4 -4
- package/migrations/supabase/20260330195700_install_mcp_sessions.sql +67 -3
- package/migrations/supabase/20260421010000_add_session_cleanup_cron.sql +6 -6
- package/package.json +13 -3
- package/src/client/core/sse-client.ts +2 -2
- package/src/client/react/index.ts +1 -1
- package/src/client/react/oauth-popup.tsx +34 -13
- package/src/client/react/use-mcp.ts +19 -45
- package/src/client/utils/session-state.ts +43 -0
- package/src/client/vue/use-mcp.ts +18 -47
- package/src/server/handlers/sse-handler.ts +9 -4
- package/src/server/mcp/multi-session-client.ts +2 -6
- package/src/server/mcp/oauth-client.ts +73 -101
- package/src/server/mcp/storage-oauth-provider.ts +79 -50
- package/src/server/storage/file-backend.ts +74 -18
- package/src/server/storage/index.ts +2 -4
- package/src/server/storage/memory-backend.ts +49 -13
- package/src/server/storage/neon-backend.ts +201 -68
- package/src/server/storage/redis-backend.ts +81 -23
- package/src/server/storage/session-lifecycle.ts +78 -0
- package/src/server/storage/sqlite-backend.ts +89 -15
- package/src/server/storage/supabase-backend.ts +188 -63
- package/src/server/storage/types.ts +49 -16
- package/src/shared/constants.ts +5 -6
- package/src/shared/types.ts +5 -3
- package/src/shared/utils.ts +26 -0
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
|
|
8
8
|
import { SSEClient, type SSEClientOptions } from '../core/sse-client';
|
|
9
|
+
import {
|
|
10
|
+
getInitialConnectionState,
|
|
11
|
+
getVisibleConnectionState,
|
|
12
|
+
isTransientReconnectState,
|
|
13
|
+
} from '../utils/session-state';
|
|
9
14
|
import type { McpConnectionEvent, McpConnectionState } from '../../shared/events';
|
|
10
15
|
import type {
|
|
11
16
|
ToolInfo,
|
|
@@ -84,6 +89,7 @@ export interface McpConnection {
|
|
|
84
89
|
authUrl?: string;
|
|
85
90
|
error?: string;
|
|
86
91
|
createdAt?: Date;
|
|
92
|
+
updatedAt?: Date;
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
export interface McpClient {
|
|
@@ -167,7 +173,7 @@ export interface McpClient {
|
|
|
167
173
|
/**
|
|
168
174
|
* Complete OAuth authorization
|
|
169
175
|
*/
|
|
170
|
-
finishAuth: (
|
|
176
|
+
finishAuth: (state: string, code: string) => Promise<FinishAuthResult>;
|
|
171
177
|
|
|
172
178
|
/**
|
|
173
179
|
* Explicitly resume OAuth flow for an existing session
|
|
@@ -293,42 +299,6 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
293
299
|
const updateConnectionsFromEvent = useCallback((event: McpConnectionEvent) => {
|
|
294
300
|
if (!isMountedRef.current) return;
|
|
295
301
|
|
|
296
|
-
const isTransientReconnectState = (state: McpConnectionState): boolean =>
|
|
297
|
-
state === 'INITIALIZING' ||
|
|
298
|
-
state === 'VALIDATING' ||
|
|
299
|
-
state === 'RECONNECTING' ||
|
|
300
|
-
state === 'CONNECTING' ||
|
|
301
|
-
state === 'CONNECTED' ||
|
|
302
|
-
state === 'DISCOVERING';
|
|
303
|
-
|
|
304
|
-
const getVisibleState = (
|
|
305
|
-
incomingState: McpConnectionState,
|
|
306
|
-
existingState?: McpConnectionState,
|
|
307
|
-
previousState?: McpConnectionState
|
|
308
|
-
): McpConnectionState => {
|
|
309
|
-
// `INITIALIZING` has two meanings in practice:
|
|
310
|
-
// 1. genuine cold start / reconnect work
|
|
311
|
-
// 2. an internal setup step that happens mid-OAuth completion
|
|
312
|
-
//
|
|
313
|
-
// For case (2), showing raw `INITIALIZING` creates a confusing user-facing
|
|
314
|
-
// sequence like AUTHENTICATING -> INITIALIZING -> AUTHENTICATED.
|
|
315
|
-
// We keep the raw event stream intact for observability, but collapse the
|
|
316
|
-
// visible state back into the current auth phase in the UI.
|
|
317
|
-
if (
|
|
318
|
-
incomingState === 'INITIALIZING' &&
|
|
319
|
-
(existingState === 'AUTHENTICATING' ||
|
|
320
|
-
existingState === 'AUTHENTICATED' ||
|
|
321
|
-
previousState === 'AUTHENTICATING' ||
|
|
322
|
-
previousState === 'AUTHENTICATED')
|
|
323
|
-
) {
|
|
324
|
-
return existingState === 'AUTHENTICATED' || previousState === 'AUTHENTICATED'
|
|
325
|
-
? 'AUTHENTICATED'
|
|
326
|
-
: 'AUTHENTICATING';
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return incomingState;
|
|
330
|
-
};
|
|
331
|
-
|
|
332
302
|
setConnections((prev: McpConnection[]) => {
|
|
333
303
|
switch (event.type) {
|
|
334
304
|
case 'state_changed': {
|
|
@@ -336,7 +306,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
336
306
|
if (existing) {
|
|
337
307
|
// Normalize the incoming backend state into the smoother user-facing
|
|
338
308
|
// state we want to render for this existing connection.
|
|
339
|
-
const normalizedState =
|
|
309
|
+
const normalizedState = getVisibleConnectionState(event.state, existing.state, event.previousState);
|
|
340
310
|
// In stateless per-request transport, tool calls can emit transient reconnect states.
|
|
341
311
|
// Keep READY sticky to avoid UI flicker from READY -> CONNECTING -> CONNECTED.
|
|
342
312
|
const nextState =
|
|
@@ -344,12 +314,14 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
344
314
|
? existing.state
|
|
345
315
|
: normalizedState;
|
|
346
316
|
|
|
317
|
+
const updatedAt = new Date();
|
|
347
318
|
return prev.map((c: McpConnection) =>
|
|
348
319
|
c.sessionId === event.sessionId ? {
|
|
349
320
|
...c,
|
|
350
321
|
state: nextState,
|
|
351
322
|
// update createdAt if present in event, otherwise keep existing
|
|
352
|
-
createdAt: event.createdAt ? new Date(event.createdAt) : c.createdAt
|
|
323
|
+
createdAt: event.createdAt ? new Date(event.createdAt) : c.createdAt,
|
|
324
|
+
updatedAt,
|
|
353
325
|
} : c
|
|
354
326
|
);
|
|
355
327
|
} else {
|
|
@@ -367,8 +339,9 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
367
339
|
serverUrl: event.serverUrl,
|
|
368
340
|
// New connections do not have prior local state, so we normalize
|
|
369
341
|
// only against the server-reported previous state.
|
|
370
|
-
state:
|
|
342
|
+
state: getVisibleConnectionState(event.state, undefined, event.previousState),
|
|
371
343
|
createdAt: event.createdAt ? new Date(event.createdAt) : undefined,
|
|
344
|
+
updatedAt: new Date(),
|
|
372
345
|
tools: [],
|
|
373
346
|
},
|
|
374
347
|
];
|
|
@@ -382,7 +355,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
382
355
|
}
|
|
383
356
|
|
|
384
357
|
return prev.map((c: McpConnection) =>
|
|
385
|
-
c.sessionId === event.sessionId ? { ...c, tools: event.tools, state: 'READY' } : c
|
|
358
|
+
c.sessionId === event.sessionId ? { ...c, tools: event.tools, state: 'READY', updatedAt: new Date() } : c
|
|
386
359
|
);
|
|
387
360
|
}
|
|
388
361
|
|
|
@@ -453,8 +426,9 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
453
426
|
serverName: s.serverName ?? 'Unknown Server',
|
|
454
427
|
serverUrl: s.serverUrl,
|
|
455
428
|
transport: s.transport,
|
|
456
|
-
state: (s.
|
|
429
|
+
state: getInitialConnectionState(s.status),
|
|
457
430
|
createdAt: new Date(s.createdAt),
|
|
431
|
+
updatedAt: new Date(s.updatedAt ?? s.createdAt),
|
|
458
432
|
tools: [],
|
|
459
433
|
}))
|
|
460
434
|
);
|
|
@@ -466,7 +440,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
466
440
|
if (clientRef.current) {
|
|
467
441
|
try {
|
|
468
442
|
// Pending auth sessions should not auto-trigger popup/redirect on reload.
|
|
469
|
-
if (session.
|
|
443
|
+
if (session.status !== 'active') {
|
|
470
444
|
return;
|
|
471
445
|
}
|
|
472
446
|
suppressAuthRedirectSessionsRef.current.add(session.sessionId);
|
|
@@ -585,12 +559,12 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
585
559
|
/**
|
|
586
560
|
* Complete OAuth authorization
|
|
587
561
|
*/
|
|
588
|
-
const finishAuth = useCallback(async (
|
|
562
|
+
const finishAuth = useCallback(async (state: string, code: string): Promise<FinishAuthResult> => {
|
|
589
563
|
if (!clientRef.current) {
|
|
590
564
|
throw new Error('SSE client not initialized');
|
|
591
565
|
}
|
|
592
566
|
|
|
593
|
-
return await clientRef.current.finishAuth(
|
|
567
|
+
return await clientRef.current.finishAuth(state, code);
|
|
594
568
|
}, []);
|
|
595
569
|
|
|
596
570
|
/**
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { McpConnectionState } from '../../shared/events';
|
|
2
|
+
import type { SessionInfo } from '../../shared/types';
|
|
3
|
+
|
|
4
|
+
export function getInitialConnectionState(status: SessionInfo['status']): McpConnectionState {
|
|
5
|
+
return status === 'active' ? 'VALIDATING' : 'AUTHENTICATING';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isTransientReconnectState(state: McpConnectionState): boolean {
|
|
9
|
+
return state === 'INITIALIZING' ||
|
|
10
|
+
state === 'VALIDATING' ||
|
|
11
|
+
state === 'RECONNECTING' ||
|
|
12
|
+
state === 'CONNECTING' ||
|
|
13
|
+
state === 'CONNECTED' ||
|
|
14
|
+
state === 'DISCOVERING';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getVisibleConnectionState(
|
|
18
|
+
incomingState: McpConnectionState,
|
|
19
|
+
existingState?: McpConnectionState,
|
|
20
|
+
previousState?: McpConnectionState
|
|
21
|
+
): McpConnectionState {
|
|
22
|
+
// `INITIALIZING` has two meanings in practice:
|
|
23
|
+
// 1. genuine cold start / reconnect work
|
|
24
|
+
// 2. an internal setup step that happens mid-OAuth completion
|
|
25
|
+
//
|
|
26
|
+
// For case (2), showing raw `INITIALIZING` creates a confusing user-facing
|
|
27
|
+
// sequence like AUTHENTICATING -> INITIALIZING -> AUTHENTICATED.
|
|
28
|
+
if (
|
|
29
|
+
incomingState === 'INITIALIZING' &&
|
|
30
|
+
(
|
|
31
|
+
existingState === 'AUTHENTICATING' ||
|
|
32
|
+
existingState === 'AUTHENTICATED' ||
|
|
33
|
+
previousState === 'AUTHENTICATING' ||
|
|
34
|
+
previousState === 'AUTHENTICATED'
|
|
35
|
+
)
|
|
36
|
+
) {
|
|
37
|
+
return existingState === 'AUTHENTICATED' || previousState === 'AUTHENTICATED'
|
|
38
|
+
? 'AUTHENTICATED'
|
|
39
|
+
: 'AUTHENTICATING';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return incomingState;
|
|
43
|
+
}
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
import { ref, onMounted, onUnmounted, watch, computed, shallowRef } from 'vue';
|
|
8
8
|
import { SSEClient, type SSEClientOptions } from '../core/sse-client';
|
|
9
|
+
import {
|
|
10
|
+
getInitialConnectionState,
|
|
11
|
+
getVisibleConnectionState,
|
|
12
|
+
isTransientReconnectState,
|
|
13
|
+
} from '../utils/session-state';
|
|
9
14
|
import type { McpConnectionEvent, McpConnectionState } from '../../shared/events';
|
|
10
15
|
import type {
|
|
11
16
|
ToolInfo,
|
|
@@ -83,6 +88,7 @@ export interface McpConnection {
|
|
|
83
88
|
authUrl?: string;
|
|
84
89
|
error?: string;
|
|
85
90
|
createdAt?: Date;
|
|
91
|
+
updatedAt?: Date;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
export interface McpClient {
|
|
@@ -166,7 +172,7 @@ export interface McpClient {
|
|
|
166
172
|
/**
|
|
167
173
|
* Complete OAuth authorization
|
|
168
174
|
*/
|
|
169
|
-
finishAuth: (
|
|
175
|
+
finishAuth: (state: string, code: string) => Promise<FinishAuthResult>;
|
|
170
176
|
|
|
171
177
|
/**
|
|
172
178
|
* Explicitly resume OAuth flow for an existing session
|
|
@@ -243,51 +249,13 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
243
249
|
const updateConnectionsFromEvent = (event: McpConnectionEvent) => {
|
|
244
250
|
if (!isMountedRef.value) return;
|
|
245
251
|
|
|
246
|
-
const isTransientReconnectState = (state: McpConnectionState): boolean =>
|
|
247
|
-
state === 'INITIALIZING' ||
|
|
248
|
-
state === 'VALIDATING' ||
|
|
249
|
-
state === 'RECONNECTING' ||
|
|
250
|
-
state === 'CONNECTING' ||
|
|
251
|
-
state === 'CONNECTED' ||
|
|
252
|
-
state === 'DISCOVERING';
|
|
253
|
-
|
|
254
|
-
const getVisibleState = (
|
|
255
|
-
incomingState: McpConnectionState,
|
|
256
|
-
existingState?: McpConnectionState,
|
|
257
|
-
previousState?: McpConnectionState
|
|
258
|
-
): McpConnectionState => {
|
|
259
|
-
// `INITIALIZING` has two meanings in practice:
|
|
260
|
-
// 1. genuine cold start / reconnect work
|
|
261
|
-
// 2. an internal setup step that happens mid-OAuth completion
|
|
262
|
-
//
|
|
263
|
-
// For case (2), showing raw `INITIALIZING` creates a confusing user-facing
|
|
264
|
-
// sequence like AUTHENTICATING -> INITIALIZING -> AUTHENTICATED.
|
|
265
|
-
// We keep the raw event stream intact for observability, but collapse the
|
|
266
|
-
// visible state back into the current auth phase in the UI.
|
|
267
|
-
if (
|
|
268
|
-
incomingState === 'INITIALIZING' &&
|
|
269
|
-
(
|
|
270
|
-
existingState === 'AUTHENTICATING' ||
|
|
271
|
-
existingState === 'AUTHENTICATED' ||
|
|
272
|
-
previousState === 'AUTHENTICATING' ||
|
|
273
|
-
previousState === 'AUTHENTICATED'
|
|
274
|
-
)
|
|
275
|
-
) {
|
|
276
|
-
return existingState === 'AUTHENTICATED' || previousState === 'AUTHENTICATED'
|
|
277
|
-
? 'AUTHENTICATED'
|
|
278
|
-
: 'AUTHENTICATING';
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return incomingState;
|
|
282
|
-
};
|
|
283
|
-
|
|
284
252
|
switch (event.type) {
|
|
285
253
|
case 'state_changed': {
|
|
286
254
|
const existing = connections.value.find((c) => c.sessionId === event.sessionId);
|
|
287
255
|
if (existing) {
|
|
288
256
|
// Normalize the incoming backend state into the smoother user-facing
|
|
289
257
|
// state we want to render for this existing connection.
|
|
290
|
-
const normalizedState =
|
|
258
|
+
const normalizedState = getVisibleConnectionState(event.state, existing.state, event.previousState);
|
|
291
259
|
// In stateless per-request transport, tool calls can emit transient reconnect states.
|
|
292
260
|
// Keep READY sticky to avoid UI flicker from READY -> CONNECTING -> CONNECTED.
|
|
293
261
|
const nextState =
|
|
@@ -300,7 +268,8 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
300
268
|
...existing,
|
|
301
269
|
state: nextState,
|
|
302
270
|
// update createdAt if present in event, otherwise keep existing
|
|
303
|
-
createdAt: event.createdAt ? new Date(event.createdAt) : existing.createdAt
|
|
271
|
+
createdAt: event.createdAt ? new Date(event.createdAt) : existing.createdAt,
|
|
272
|
+
updatedAt: new Date(),
|
|
304
273
|
};
|
|
305
274
|
} else {
|
|
306
275
|
// Fix: Don't add back disconnected sessions that were just removed
|
|
@@ -314,8 +283,9 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
314
283
|
serverName: event.serverName,
|
|
315
284
|
// New connections do not have prior local state, so we normalize
|
|
316
285
|
// only against the server-reported previous state.
|
|
317
|
-
state:
|
|
286
|
+
state: getVisibleConnectionState(event.state, undefined, event.previousState),
|
|
318
287
|
createdAt: event.createdAt ? new Date(event.createdAt) : undefined,
|
|
288
|
+
updatedAt: new Date(),
|
|
319
289
|
tools: [],
|
|
320
290
|
}];
|
|
321
291
|
}
|
|
@@ -325,7 +295,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
325
295
|
case 'tools_discovered': {
|
|
326
296
|
const index = connections.value.findIndex((c) => c.sessionId === event.sessionId);
|
|
327
297
|
if (index !== -1) {
|
|
328
|
-
connections.value[index] = { ...connections.value[index], tools: event.tools, state: 'READY' };
|
|
298
|
+
connections.value[index] = { ...connections.value[index], tools: event.tools, state: 'READY', updatedAt: new Date() };
|
|
329
299
|
}
|
|
330
300
|
break;
|
|
331
301
|
}
|
|
@@ -397,8 +367,9 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
397
367
|
serverName: s.serverName ?? 'Unknown Server',
|
|
398
368
|
serverUrl: s.serverUrl,
|
|
399
369
|
transport: s.transport,
|
|
400
|
-
state: (s.
|
|
370
|
+
state: getInitialConnectionState(s.status),
|
|
401
371
|
createdAt: new Date(s.createdAt),
|
|
372
|
+
updatedAt: new Date(s.updatedAt ?? s.createdAt),
|
|
402
373
|
tools: [],
|
|
403
374
|
}));
|
|
404
375
|
}
|
|
@@ -409,7 +380,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
409
380
|
if (clientRef.value) {
|
|
410
381
|
try {
|
|
411
382
|
// Pending auth sessions should not auto-trigger popup/redirect on reload.
|
|
412
|
-
if (session.
|
|
383
|
+
if (session.status !== 'active') {
|
|
413
384
|
return;
|
|
414
385
|
}
|
|
415
386
|
suppressAuthRedirectSessions.value.add(session.sessionId);
|
|
@@ -573,12 +544,12 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
573
544
|
/**
|
|
574
545
|
* Complete OAuth authorization
|
|
575
546
|
*/
|
|
576
|
-
const finishAuth = async (
|
|
547
|
+
const finishAuth = async (state: string, code: string): Promise<FinishAuthResult> => {
|
|
577
548
|
if (!clientRef.value) {
|
|
578
549
|
throw new Error('SSE client not initialized');
|
|
579
550
|
}
|
|
580
551
|
|
|
581
|
-
return await clientRef.value.finishAuth(
|
|
552
|
+
return await clientRef.value.finishAuth(state, code);
|
|
582
553
|
};
|
|
583
554
|
|
|
584
555
|
/**
|
|
@@ -36,6 +36,7 @@ import type {
|
|
|
36
36
|
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
|
+
import { parseOAuthState } from '../../shared/utils.js';
|
|
39
40
|
import { MCPClient } from '../mcp/oauth-client.js';
|
|
40
41
|
import { sessions } from '../storage/index.js';
|
|
41
42
|
|
|
@@ -238,7 +239,8 @@ export class SSEConnectionManager {
|
|
|
238
239
|
serverUrl: s.serverUrl,
|
|
239
240
|
transport: s.transportType,
|
|
240
241
|
createdAt: s.createdAt,
|
|
241
|
-
|
|
242
|
+
updatedAt: s.updatedAt ?? s.createdAt,
|
|
243
|
+
status: s.status ?? 'pending',
|
|
242
244
|
})),
|
|
243
245
|
};
|
|
244
246
|
}
|
|
@@ -265,7 +267,7 @@ export class SSEConnectionManager {
|
|
|
265
267
|
if (duplicate) {
|
|
266
268
|
// If the existing session is still pending OAuth, treat connect as "resume auth"
|
|
267
269
|
// instead of failing with duplicate connection error.
|
|
268
|
-
if (duplicate.
|
|
270
|
+
if (duplicate.status === 'pending') {
|
|
269
271
|
await this.getSession({ sessionId: duplicate.sessionId });
|
|
270
272
|
return {
|
|
271
273
|
sessionId: duplicate.sessionId,
|
|
@@ -504,7 +506,10 @@ export class SSEConnectionManager {
|
|
|
504
506
|
* Complete OAuth authorization flow
|
|
505
507
|
*/
|
|
506
508
|
private async finishAuth(params: FinishAuthParams): Promise<FinishAuthResult> {
|
|
507
|
-
const {
|
|
509
|
+
const { code } = params;
|
|
510
|
+
const oauthState = params.state;
|
|
511
|
+
const parsedState = parseOAuthState(oauthState);
|
|
512
|
+
const sessionId = parsedState?.sessionId || oauthState;
|
|
508
513
|
|
|
509
514
|
const session = await sessions.get(this.userId, sessionId);
|
|
510
515
|
if (!session) {
|
|
@@ -533,7 +538,7 @@ export class SSEConnectionManager {
|
|
|
533
538
|
|
|
534
539
|
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
535
540
|
|
|
536
|
-
await client.finishAuth(code);
|
|
541
|
+
await client.finishAuth(code, oauthState);
|
|
537
542
|
this.clients.set(sessionId, client);
|
|
538
543
|
|
|
539
544
|
const tools = await client.listTools();
|
|
@@ -66,12 +66,8 @@ export class MultiSessionClient {
|
|
|
66
66
|
*
|
|
67
67
|
* A session is considered connectable when:
|
|
68
68
|
* - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
|
|
69
|
-
* - Its
|
|
70
|
-
* either mid-OAuth flow, auth-pending, or previously failed. We skip those here
|
|
69
|
+
* - Its status is `active`. Pending sessions are skipped here
|
|
71
70
|
* and let the OAuth flow complete separately before we try to reconnect them.
|
|
72
|
-
*
|
|
73
|
-
* Note: Sessions where `active` is `undefined` (legacy records) are included
|
|
74
|
-
* for backwards compatibility.
|
|
75
71
|
*/
|
|
76
72
|
private async getActiveSessions(): Promise<Session[]> {
|
|
77
73
|
const sessionList = await sessions.list(this.userId);
|
|
@@ -79,7 +75,7 @@ export class MultiSessionClient {
|
|
|
79
75
|
s.serverId &&
|
|
80
76
|
s.serverUrl &&
|
|
81
77
|
s.callbackUrl &&
|
|
82
|
-
s.
|
|
78
|
+
s.status === 'active'
|
|
83
79
|
);
|
|
84
80
|
return valid;
|
|
85
81
|
}
|