@mcp-ts/sdk 1.3.1 → 1.3.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.
Files changed (63) hide show
  1. package/README.md +371 -290
  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 +3 -3
  11. package/dist/adapters/mastra-adapter.d.ts +3 -3
  12. package/dist/client/index.d.mts +10 -66
  13. package/dist/client/index.d.ts +10 -66
  14. package/dist/client/index.js +91 -173
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/index.mjs +91 -173
  17. package/dist/client/index.mjs.map +1 -1
  18. package/dist/client/react.d.mts +15 -5
  19. package/dist/client/react.d.ts +15 -5
  20. package/dist/client/react.js +130 -182
  21. package/dist/client/react.js.map +1 -1
  22. package/dist/client/react.mjs +130 -182
  23. package/dist/client/react.mjs.map +1 -1
  24. package/dist/client/vue.d.mts +27 -7
  25. package/dist/client/vue.d.ts +27 -7
  26. package/dist/client/vue.js +131 -182
  27. package/dist/client/vue.js.map +1 -1
  28. package/dist/client/vue.mjs +131 -182
  29. package/dist/client/vue.mjs.map +1 -1
  30. package/dist/{events-BgeztGYZ.d.mts → events-CK3N--3g.d.mts} +2 -0
  31. package/dist/{events-BgeztGYZ.d.ts → events-CK3N--3g.d.ts} +2 -0
  32. package/dist/index.d.mts +3 -3
  33. package/dist/index.d.ts +3 -3
  34. package/dist/index.js +224 -258
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +224 -258
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{multi-session-client-CxogNckF.d.mts → multi-session-client-DzjmT7FX.d.mts} +4 -10
  39. package/dist/{multi-session-client-cox_WXUj.d.ts → multi-session-client-FAFpUzZ4.d.ts} +4 -10
  40. package/dist/server/index.d.mts +18 -23
  41. package/dist/server/index.d.ts +18 -23
  42. package/dist/server/index.js +133 -85
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +133 -85
  45. package/dist/server/index.mjs.map +1 -1
  46. package/dist/shared/index.d.mts +3 -3
  47. package/dist/shared/index.d.ts +3 -3
  48. package/dist/shared/index.js.map +1 -1
  49. package/dist/shared/index.mjs.map +1 -1
  50. package/dist/{types-CLccx9wW.d.mts → types-CW6lghof.d.mts} +6 -0
  51. package/dist/{types-CLccx9wW.d.ts → types-CW6lghof.d.ts} +6 -0
  52. package/package.json +1 -1
  53. package/src/client/core/sse-client.ts +354 -493
  54. package/src/client/react/index.ts +16 -16
  55. package/src/client/react/use-mcp-apps.tsx +214 -214
  56. package/src/client/react/use-mcp.ts +84 -19
  57. package/src/client/vue/use-mcp.ts +119 -44
  58. package/src/server/handlers/nextjs-handler.ts +207 -217
  59. package/src/server/handlers/sse-handler.ts +14 -0
  60. package/src/server/mcp/oauth-client.ts +48 -46
  61. package/src/server/storage/types.ts +12 -5
  62. package/src/shared/events.ts +2 -0
  63. package/src/shared/types.ts +6 -0
@@ -1,9 +1,9 @@
1
1
  import { SSEClient, AppHost } from './index.js';
2
2
  export { SSEClientOptions } from './index.js';
3
- import { c as McpConnectionState, M as McpConnectionEvent } from '../events-BgeztGYZ.js';
4
- export { D as Disposable, a as DisposableStore, E as Emitter, b as Event, d as McpObservabilityEvent } from '../events-BgeztGYZ.js';
5
- import { T as ToolInfo, k as FinishAuthResult, n as ListToolsRpcResult, L as ListPromptsResult, l as ListResourcesResult } from '../types-CLccx9wW.js';
6
- export { p as McpRpcRequest, q as McpRpcResponse } from '../types-CLccx9wW.js';
3
+ import { c as McpConnectionState, M as McpConnectionEvent } from '../events-CK3N--3g.js';
4
+ export { D as Disposable, a as DisposableStore, E as Emitter, b as Event, d as McpObservabilityEvent } from '../events-CK3N--3g.js';
5
+ import { T as ToolInfo, k as FinishAuthResult, n as ListToolsRpcResult, L as ListPromptsResult, l as ListResourcesResult } from '../types-CW6lghof.js';
6
+ export { p as McpRpcRequest, q as McpRpcResponse } from '../types-CW6lghof.js';
7
7
  import React$1 from 'react';
8
8
  import '@modelcontextprotocol/sdk/types.js';
9
9
 
@@ -54,6 +54,11 @@ interface UseMcpOptions {
54
54
  * @default 60000
55
55
  */
56
56
  requestTimeout?: number;
57
+ /**
58
+ * Enable client debug logs.
59
+ * @default false
60
+ */
61
+ debug?: boolean;
57
62
  }
58
63
  interface McpConnection {
59
64
  sessionId: string;
@@ -63,8 +68,9 @@ interface McpConnection {
63
68
  transport?: string;
64
69
  state: McpConnectionState;
65
70
  tools: ToolInfo[];
71
+ authUrl?: string;
66
72
  error?: string;
67
- connectedAt?: Date;
73
+ createdAt?: Date;
68
74
  }
69
75
  interface McpClient$1 {
70
76
  /**
@@ -125,6 +131,10 @@ interface McpClient$1 {
125
131
  * Complete OAuth authorization
126
132
  */
127
133
  finishAuth: (sessionId: string, code: string) => Promise<FinishAuthResult>;
134
+ /**
135
+ * Explicitly resume OAuth flow for an existing session
136
+ */
137
+ resumeAuth: (sessionId: string) => Promise<void>;
128
138
  /**
129
139
  * Call a tool from a session
130
140
  */
@@ -8,62 +8,27 @@ var jsxRuntime = require('react/jsx-runtime');
8
8
  var __defProp = Object.defineProperty;
9
9
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
10
10
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
11
- var DEFAULT_REQUEST_TIMEOUT = 6e4;
12
- var MAX_RECONNECT_ATTEMPTS = 5;
13
- var BASE_RECONNECT_DELAY = 1e3;
14
11
  var SSEClient = class {
15
12
  constructor(options) {
16
13
  this.options = options;
17
- __publicField(this, "eventSource", null);
18
- __publicField(this, "pendingRequests", /* @__PURE__ */ new Map());
19
14
  __publicField(this, "resourceCache", /* @__PURE__ */ new Map());
20
- __publicField(this, "reconnectAttempts", 0);
21
- __publicField(this, "isManuallyDisconnected", false);
22
- __publicField(this, "connectionPromise", null);
23
- __publicField(this, "connectionResolver", null);
15
+ __publicField(this, "connected", false);
24
16
  }
25
- // ============================================
26
- // Connection Management
27
- // ============================================
28
- /**
29
- * Connect to the SSE endpoint
30
- */
31
17
  connect() {
32
- if (this.eventSource) {
18
+ if (this.connected) {
33
19
  return;
34
20
  }
35
- this.isManuallyDisconnected = false;
36
- this.options.onStatusChange?.("connecting");
37
- this.connectionPromise = new Promise((resolve) => {
38
- this.connectionResolver = resolve;
39
- });
40
- const url = this.buildUrl();
41
- this.eventSource = new EventSource(url);
42
- this.setupEventListeners();
21
+ this.connected = true;
22
+ this.options.onStatusChange?.("connected");
23
+ this.log("RPC mode: post_stream");
43
24
  }
44
- /**
45
- * Disconnect from the SSE endpoint
46
- */
47
25
  disconnect() {
48
- this.isManuallyDisconnected = true;
49
- if (this.eventSource) {
50
- this.eventSource.close();
51
- this.eventSource = null;
52
- }
53
- this.connectionPromise = null;
54
- this.connectionResolver = null;
55
- this.rejectAllPendingRequests(new Error("Connection closed"));
26
+ this.connected = false;
56
27
  this.options.onStatusChange?.("disconnected");
57
28
  }
58
- /**
59
- * Check if connected to the SSE endpoint
60
- */
61
29
  isConnected() {
62
- return this.eventSource?.readyState === EventSource.OPEN;
30
+ return this.connected;
63
31
  }
64
- // ============================================
65
- // RPC Methods
66
- // ============================================
67
32
  async getSessions() {
68
33
  return this.sendRequest("getSessions");
69
34
  }
@@ -99,22 +64,10 @@ var SSEClient = class {
99
64
  async readResource(sessionId, uri) {
100
65
  return this.sendRequest("readResource", { sessionId, uri });
101
66
  }
102
- // ============================================
103
- // Resource Preloading (for instant UI loading)
104
- // ============================================
105
- /**
106
- * Preload UI resources for tools that have UI metadata.
107
- * Call this when tools are discovered to enable instant MCP App UI loading.
108
- */
109
67
  preloadToolUiResources(sessionId, tools) {
110
68
  for (const tool of tools) {
111
69
  const uri = this.extractUiResourceUri(tool);
112
- if (!uri) continue;
113
- if (this.resourceCache.has(uri)) {
114
- this.log(`Resource already cached: ${uri}`);
115
- continue;
116
- }
117
- this.log(`Preloading UI resource for tool "${tool.name}": ${uri}`);
70
+ if (!uri || this.resourceCache.has(uri)) continue;
118
71
  const promise = this.sendRequest("readResource", { sessionId, uri }).catch((err) => {
119
72
  this.log(`Failed to preload resource ${uri}: ${err.message}`, "warn");
120
73
  this.resourceCache.delete(uri);
@@ -123,43 +76,24 @@ var SSEClient = class {
123
76
  this.resourceCache.set(uri, promise);
124
77
  }
125
78
  }
126
- /**
127
- * Get a preloaded resource from cache, or fetch if not cached.
128
- */
129
79
  getOrFetchResource(sessionId, uri) {
130
80
  const cached = this.resourceCache.get(uri);
131
- if (cached) {
132
- this.log(`Cache hit for resource: ${uri}`);
133
- return cached;
134
- }
135
- this.log(`Cache miss, fetching resource: ${uri}`);
81
+ if (cached) return cached;
136
82
  const promise = this.sendRequest("readResource", { sessionId, uri });
137
83
  this.resourceCache.set(uri, promise);
138
84
  return promise;
139
85
  }
140
- /**
141
- * Check if a resource is already cached
142
- */
143
86
  hasPreloadedResource(uri) {
144
87
  return this.resourceCache.has(uri);
145
88
  }
146
- /**
147
- * Clear the resource cache
148
- */
149
89
  clearResourceCache() {
150
90
  this.resourceCache.clear();
151
91
  }
152
- // ============================================
153
- // Private: Request Handling
154
- // ============================================
155
- /**
156
- * Send an RPC request and return the response directly from HTTP.
157
- * This bypasses SSE latency by returning results in the HTTP response body.
158
- */
159
92
  async sendRequest(method, params) {
160
- if (this.connectionPromise) {
161
- await this.connectionPromise;
93
+ if (!this.connected) {
94
+ this.connect();
162
95
  }
96
+ this.log(`RPC request via post_stream: ${method}`);
163
97
  const request = {
164
98
  id: `rpc_${nanoid.nanoid(10)}`,
165
99
  method,
@@ -173,103 +107,93 @@ var SSEClient = class {
173
107
  if (!response.ok) {
174
108
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
175
109
  }
176
- const data = await response.json();
177
- return this.parseRpcResponse(data, request.id);
110
+ const contentType = (response.headers.get("content-type") || "").toLowerCase();
111
+ if (!contentType.includes("text/event-stream")) {
112
+ const data2 = await response.json();
113
+ return this.parseRpcResponse(data2);
114
+ }
115
+ const data = await this.readRpcResponseFromStream(response);
116
+ return this.parseRpcResponse(data);
117
+ }
118
+ async readRpcResponseFromStream(response) {
119
+ if (!response.body) {
120
+ throw new Error("Streaming response body is missing");
121
+ }
122
+ const reader = response.body.getReader();
123
+ const decoder = new TextDecoder();
124
+ let buffer = "";
125
+ let rpcResponse = null;
126
+ const dispatchBlock = (block) => {
127
+ const lines = block.split("\n");
128
+ let eventName = "message";
129
+ const dataLines = [];
130
+ for (const rawLine of lines) {
131
+ const line = rawLine.replace(/\r$/, "");
132
+ if (!line || line.startsWith(":")) continue;
133
+ if (line.startsWith("event:")) {
134
+ eventName = line.slice("event:".length).trim();
135
+ continue;
136
+ }
137
+ if (line.startsWith("data:")) {
138
+ dataLines.push(line.slice("data:".length).trimStart());
139
+ }
140
+ }
141
+ if (!dataLines.length) return;
142
+ const payloadText = dataLines.join("\n");
143
+ let payload = payloadText;
144
+ try {
145
+ payload = JSON.parse(payloadText);
146
+ } catch {
147
+ }
148
+ switch (eventName) {
149
+ case "connected":
150
+ this.options.onStatusChange?.("connected");
151
+ break;
152
+ case "connection":
153
+ this.options.onConnectionEvent?.(payload);
154
+ break;
155
+ case "observability":
156
+ this.options.onObservabilityEvent?.(payload);
157
+ break;
158
+ case "rpc-response":
159
+ rpcResponse = payload;
160
+ break;
161
+ }
162
+ };
163
+ while (true) {
164
+ const { value, done } = await reader.read();
165
+ if (done) break;
166
+ buffer += decoder.decode(value, { stream: true });
167
+ let separatorMatch = buffer.match(/\r?\n\r?\n/);
168
+ while (separatorMatch && separatorMatch.index !== void 0) {
169
+ const separatorIndex = separatorMatch.index;
170
+ const separatorLength = separatorMatch[0].length;
171
+ const block = buffer.slice(0, separatorIndex);
172
+ buffer = buffer.slice(separatorIndex + separatorLength);
173
+ dispatchBlock(block);
174
+ separatorMatch = buffer.match(/\r?\n\r?\n/);
175
+ }
176
+ }
177
+ if (buffer.trim()) {
178
+ dispatchBlock(buffer);
179
+ }
180
+ if (!rpcResponse) {
181
+ throw new Error("Missing rpc-response event in streamed RPC result");
182
+ }
183
+ return rpcResponse;
178
184
  }
179
- /**
180
- * Parse RPC response and handle different response formats
181
- */
182
- parseRpcResponse(data, requestId) {
185
+ parseRpcResponse(data) {
183
186
  if ("result" in data) {
184
187
  return data.result;
185
188
  }
186
189
  if ("error" in data && data.error) {
187
190
  throw new Error(data.error.message || "Unknown RPC error");
188
191
  }
189
- if ("acknowledged" in data) {
190
- return this.waitForSseResponse(requestId);
192
+ if (data && typeof data === "object" && "id" in data) {
193
+ return void 0;
191
194
  }
192
195
  throw new Error("Invalid RPC response format");
193
196
  }
194
- /**
195
- * Wait for RPC response via SSE (legacy fallback)
196
- */
197
- waitForSseResponse(requestId) {
198
- const timeoutMs = this.options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
199
- return new Promise((resolve, reject) => {
200
- const timeoutId = setTimeout(() => {
201
- this.pendingRequests.delete(requestId);
202
- reject(new Error(`Request timeout after ${timeoutMs}ms`));
203
- }, timeoutMs);
204
- this.pendingRequests.set(requestId, {
205
- resolve,
206
- reject,
207
- timeoutId
208
- });
209
- });
210
- }
211
- /**
212
- * Handle RPC response received via SSE (legacy)
213
- */
214
- handleRpcResponse(response) {
215
- const pending = this.pendingRequests.get(response.id);
216
- if (!pending) return;
217
- clearTimeout(pending.timeoutId);
218
- this.pendingRequests.delete(response.id);
219
- if (response.error) {
220
- pending.reject(new Error(response.error.message));
221
- } else {
222
- pending.resolve(response.result);
223
- }
224
- }
225
- // ============================================
226
- // Private: Event Handling
227
- // ============================================
228
- setupEventListeners() {
229
- if (!this.eventSource) return;
230
- this.eventSource.addEventListener("open", () => {
231
- this.log("Connected");
232
- this.reconnectAttempts = 0;
233
- this.options.onStatusChange?.("connected");
234
- });
235
- this.eventSource.addEventListener("connected", () => {
236
- this.log("Server ready");
237
- this.connectionResolver?.();
238
- this.connectionResolver = null;
239
- });
240
- this.eventSource.addEventListener("connection", (e) => {
241
- const event = JSON.parse(e.data);
242
- this.options.onConnectionEvent?.(event);
243
- });
244
- this.eventSource.addEventListener("observability", (e) => {
245
- const event = JSON.parse(e.data);
246
- this.options.onObservabilityEvent?.(event);
247
- });
248
- this.eventSource.addEventListener("rpc-response", (e) => {
249
- const response = JSON.parse(e.data);
250
- this.handleRpcResponse(response);
251
- });
252
- this.eventSource.addEventListener("error", () => {
253
- this.log("Connection error", "error");
254
- this.options.onStatusChange?.("error");
255
- this.attemptReconnect();
256
- });
257
- }
258
- attemptReconnect() {
259
- if (this.isManuallyDisconnected || this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
260
- return;
261
- }
262
- this.reconnectAttempts++;
263
- const delay = BASE_RECONNECT_DELAY * this.reconnectAttempts;
264
- this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
265
- setTimeout(() => {
266
- this.disconnect();
267
- this.connect();
268
- }, delay);
269
- }
270
- // ============================================
271
- // Private: Utilities
272
- // ============================================
273
197
  buildUrl() {
274
198
  const url = new URL(this.options.url, globalThis.location?.origin);
275
199
  url.searchParams.set("identity", this.options.identity);
@@ -280,20 +204,14 @@ var SSEClient = class {
280
204
  }
281
205
  buildHeaders() {
282
206
  const headers = {
283
- "Content-Type": "application/json"
207
+ "Content-Type": "application/json",
208
+ "Accept": "text/event-stream"
284
209
  };
285
210
  if (this.options.authToken) {
286
211
  headers["Authorization"] = `Bearer ${this.options.authToken}`;
287
212
  }
288
213
  return headers;
289
214
  }
290
- rejectAllPendingRequests(error) {
291
- for (const [, pending] of this.pendingRequests) {
292
- clearTimeout(pending.timeoutId);
293
- pending.reject(error);
294
- }
295
- this.pendingRequests.clear();
296
- }
297
215
  extractUiResourceUri(tool) {
298
216
  const meta = tool._meta?.ui;
299
217
  if (!meta || typeof meta !== "object") return void 0;
@@ -344,6 +262,7 @@ function useMcp(options) {
344
262
  } = options;
345
263
  const clientRef = react.useRef(null);
346
264
  const isMountedRef = react.useRef(true);
265
+ const suppressAuthRedirectSessionsRef = react.useRef(/* @__PURE__ */ new Set());
347
266
  const [connections, setConnections] = react.useState([]);
348
267
  const [status, setStatus] = react.useState(
349
268
  "disconnected"
@@ -367,7 +286,7 @@ function useMcp(options) {
367
286
  setStatus(newStatus);
368
287
  }
369
288
  },
370
- requestTimeout: options.requestTimeout
289
+ debug: options.debug
371
290
  };
372
291
  const client = new SSEClient(clientOptions);
373
292
  clientRef.current = client;
@@ -384,22 +303,34 @@ function useMcp(options) {
384
303
  }, [url, identity, authToken, autoConnect, autoInitialize]);
385
304
  const updateConnectionsFromEvent = react.useCallback((event) => {
386
305
  if (!isMountedRef.current) return;
306
+ const isTransientReconnectState = (state) => state === "INITIALIZING" || state === "VALIDATING" || state === "RECONNECTING" || state === "CONNECTING" || state === "CONNECTED" || state === "DISCOVERING";
387
307
  setConnections((prev) => {
388
308
  switch (event.type) {
389
309
  case "state_changed": {
390
310
  const existing = prev.find((c) => c.sessionId === event.sessionId);
391
311
  if (existing) {
312
+ const nextState = existing.state === "READY" && isTransientReconnectState(event.state) ? existing.state : event.state;
392
313
  return prev.map(
393
- (c) => c.sessionId === event.sessionId ? { ...c, state: event.state } : c
314
+ (c) => c.sessionId === event.sessionId ? {
315
+ ...c,
316
+ state: nextState,
317
+ // update createdAt if present in event, otherwise keep existing
318
+ createdAt: event.createdAt ? new Date(event.createdAt) : c.createdAt
319
+ } : c
394
320
  );
395
321
  } else {
322
+ if (event.state === "DISCONNECTED") {
323
+ return prev;
324
+ }
396
325
  return [
397
326
  ...prev,
398
327
  {
399
328
  sessionId: event.sessionId,
400
329
  serverId: event.serverId,
401
330
  serverName: event.serverName,
331
+ serverUrl: event.serverUrl,
402
332
  state: event.state,
333
+ createdAt: event.createdAt ? new Date(event.createdAt) : void 0,
403
334
  tools: []
404
335
  }
405
336
  ];
@@ -416,14 +347,16 @@ function useMcp(options) {
416
347
  case "auth_required": {
417
348
  if (event.authUrl) {
418
349
  onLog?.("info", `OAuth required - redirecting to ${event.authUrl}`, { authUrl: event.authUrl });
419
- if (onRedirect) {
420
- onRedirect(event.authUrl);
421
- } else if (typeof window !== "undefined") {
422
- window.location.href = event.authUrl;
350
+ if (!suppressAuthRedirectSessionsRef.current.has(event.sessionId)) {
351
+ if (onRedirect) {
352
+ onRedirect(event.authUrl);
353
+ } else if (typeof window !== "undefined") {
354
+ window.location.href = event.authUrl;
355
+ }
423
356
  }
424
357
  }
425
358
  return prev.map(
426
- (c) => c.sessionId === event.sessionId ? { ...c, state: "AUTHENTICATING" } : c
359
+ (c) => c.sessionId === event.sessionId ? { ...c, state: "AUTHENTICATING", authUrl: event.authUrl } : c
427
360
  );
428
361
  }
429
362
  case "error": {
@@ -438,7 +371,7 @@ function useMcp(options) {
438
371
  return prev;
439
372
  }
440
373
  });
441
- }, [onLog]);
374
+ }, [onLog, onRedirect]);
442
375
  const loadSessions = react.useCallback(async () => {
443
376
  if (!clientRef.current) return;
444
377
  try {
@@ -453,7 +386,8 @@ function useMcp(options) {
453
386
  serverName: s.serverName ?? "Unknown Server",
454
387
  serverUrl: s.serverUrl,
455
388
  transport: s.transport,
456
- state: "VALIDATING",
389
+ state: s.active === false ? "AUTHENTICATING" : "VALIDATING",
390
+ createdAt: new Date(s.createdAt),
457
391
  tools: []
458
392
  }))
459
393
  );
@@ -462,9 +396,15 @@ function useMcp(options) {
462
396
  sessions.map(async (session) => {
463
397
  if (clientRef.current) {
464
398
  try {
399
+ if (session.active === false) {
400
+ return;
401
+ }
402
+ suppressAuthRedirectSessionsRef.current.add(session.sessionId);
465
403
  await clientRef.current.restoreSession(session.sessionId);
466
404
  } catch (error) {
467
405
  console.error(`[useMcp] Failed to validate session ${session.sessionId}:`, error);
406
+ } finally {
407
+ suppressAuthRedirectSessionsRef.current.delete(session.sessionId);
468
408
  }
469
409
  }
470
410
  })
@@ -512,6 +452,13 @@ function useMcp(options) {
512
452
  }
513
453
  return await clientRef.current.finishAuth(sessionId, code);
514
454
  }, []);
455
+ const resumeAuth = react.useCallback(async (sessionId) => {
456
+ if (!clientRef.current) {
457
+ throw new Error("SSE client not initialized");
458
+ }
459
+ suppressAuthRedirectSessionsRef.current.delete(sessionId);
460
+ await clientRef.current.restoreSession(sessionId);
461
+ }, []);
515
462
  const callTool = react.useCallback(
516
463
  async (sessionId, toolName, toolArgs) => {
517
464
  if (!clientRef.current) {
@@ -590,6 +537,7 @@ function useMcp(options) {
590
537
  connectSSE,
591
538
  disconnectSSE,
592
539
  finishAuth,
540
+ resumeAuth,
593
541
  callTool,
594
542
  listTools,
595
543
  listPrompts,