@mcp-ts/sdk 2.3.3 → 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.
Files changed (82) hide show
  1. package/README.md +113 -30
  2. package/dist/adapters/agui-adapter.d.mts +3 -3
  3. package/dist/adapters/agui-adapter.d.ts +3 -3
  4. package/dist/adapters/agui-middleware.d.mts +3 -3
  5. package/dist/adapters/agui-middleware.d.ts +3 -3
  6. package/dist/adapters/ai-adapter.d.mts +3 -3
  7. package/dist/adapters/ai-adapter.d.ts +3 -3
  8. package/dist/adapters/langchain-adapter.d.mts +3 -3
  9. package/dist/adapters/langchain-adapter.d.ts +3 -3
  10. package/dist/adapters/mastra-adapter.d.mts +1 -1
  11. package/dist/adapters/mastra-adapter.d.ts +1 -1
  12. package/dist/client/index.d.mts +2 -2
  13. package/dist/client/index.d.ts +2 -2
  14. package/dist/client/index.js +2 -2
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/index.mjs +2 -2
  17. package/dist/client/index.mjs.map +1 -1
  18. package/dist/client/react.d.mts +9 -8
  19. package/dist/client/react.d.ts +9 -8
  20. package/dist/client/react.js +66 -26
  21. package/dist/client/react.js.map +1 -1
  22. package/dist/client/react.mjs +67 -27
  23. package/dist/client/react.mjs.map +1 -1
  24. package/dist/client/vue.d.mts +6 -5
  25. package/dist/client/vue.d.ts +6 -5
  26. package/dist/client/vue.js +27 -17
  27. package/dist/client/vue.js.map +1 -1
  28. package/dist/client/vue.mjs +27 -17
  29. package/dist/client/vue.mjs.map +1 -1
  30. package/dist/{index-Cfjsme-a.d.mts → index-ByIjEReo.d.mts} +2 -2
  31. package/dist/{index-CmjMd2ac.d.ts → index-CtXvKl8N.d.ts} +2 -2
  32. package/dist/index.d.mts +5 -5
  33. package/dist/index.d.ts +5 -5
  34. package/dist/index.js +729 -397
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +724 -396
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{multi-session-client-DYNe6az3.d.mts → multi-session-client-CIMUGF8S.d.mts} +15 -41
  39. package/dist/{multi-session-client-BYtguGJm.d.ts → multi-session-client-CnvZEGPY.d.ts} +15 -41
  40. package/dist/server/index.d.mts +45 -17
  41. package/dist/server/index.d.ts +45 -17
  42. package/dist/server/index.js +721 -391
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +718 -391
  45. package/dist/server/index.mjs.map +1 -1
  46. package/dist/shared/index.d.mts +9 -8
  47. package/dist/shared/index.d.ts +9 -8
  48. package/dist/shared/index.js +7 -5
  49. package/dist/shared/index.js.map +1 -1
  50. package/dist/shared/index.mjs +5 -4
  51. package/dist/shared/index.mjs.map +1 -1
  52. package/dist/{tool-router-BMzhoNYt.d.ts → tool-router-CZMrOG8J.d.ts} +1 -1
  53. package/dist/{tool-router-BhHsvBfP.d.mts → tool-router-CuApsDiV.d.mts} +1 -1
  54. package/dist/{types-CxFaaZrM.d.mts → types-DCk_IF4L.d.mts} +5 -3
  55. package/dist/{types-CxFaaZrM.d.ts → types-DCk_IF4L.d.ts} +5 -3
  56. package/migrations/neon/20260513010000_install_mcp_sessions.sql +40 -3
  57. package/migrations/neon/20260513020000_add_session_cleanup_cron.sql +4 -4
  58. package/migrations/supabase/20260330195700_install_mcp_sessions.sql +67 -3
  59. package/migrations/supabase/20260421010000_add_session_cleanup_cron.sql +6 -6
  60. package/package.json +13 -3
  61. package/src/client/core/sse-client.ts +2 -2
  62. package/src/client/react/index.ts +1 -1
  63. package/src/client/react/oauth-popup.tsx +34 -13
  64. package/src/client/react/use-mcp.ts +19 -45
  65. package/src/client/utils/session-state.ts +43 -0
  66. package/src/client/vue/use-mcp.ts +18 -47
  67. package/src/server/handlers/sse-handler.ts +9 -4
  68. package/src/server/mcp/multi-session-client.ts +2 -6
  69. package/src/server/mcp/oauth-client.ts +73 -220
  70. package/src/server/mcp/storage-oauth-provider.ts +79 -50
  71. package/src/server/storage/file-backend.ts +74 -18
  72. package/src/server/storage/index.ts +2 -4
  73. package/src/server/storage/memory-backend.ts +49 -13
  74. package/src/server/storage/neon-backend.ts +201 -68
  75. package/src/server/storage/redis-backend.ts +81 -23
  76. package/src/server/storage/session-lifecycle.ts +78 -0
  77. package/src/server/storage/sqlite-backend.ts +89 -15
  78. package/src/server/storage/supabase-backend.ts +188 -63
  79. package/src/server/storage/types.ts +49 -16
  80. package/src/shared/constants.ts +5 -6
  81. package/src/shared/types.ts +5 -3
  82. 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: (sessionId: string, code: string) => Promise<FinishAuthResult>;
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 = getVisibleState(event.state, existing.state, event.previousState);
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: getVisibleState(event.state, undefined, event.previousState),
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.active === false ? 'AUTHENTICATING' : 'VALIDATING') as McpConnectionState,
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.active === false) {
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 (sessionId: string, code: string): Promise<FinishAuthResult> => {
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(sessionId, code);
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: (sessionId: string, code: string) => Promise<FinishAuthResult>;
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 = getVisibleState(event.state, existing.state, event.previousState);
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: getVisibleState(event.state, undefined, event.previousState),
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.active === false ? 'AUTHENTICATING' : 'VALIDATING') as McpConnectionState,
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.active === false) {
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 (sessionId: string, code: string): Promise<FinishAuthResult> => {
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(sessionId, code);
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
- active: s.active !== false,
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.active === false) {
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 { sessionId, code } = params;
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 `active` flag is not explicitly `false` sessions with `active: false` are
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.active !== false // exclude OAuth-pending / failed sessions
78
+ s.status === 'active'
83
79
  );
84
80
  return valid;
85
81
  }