@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,16 +1,16 @@
1
- /**
2
- * MCP SDK - React Client
3
- * Simple React hooks for MCP app rendering
4
- */
5
-
6
- // Core MCP Hook
7
- export { useMcp, type UseMcpOptions, type McpClient, type McpConnection } from './use-mcp.js';
8
-
9
- // App Host (internal use)
10
- export { useAppHost } from './use-app-host.js';
11
-
12
- // Simplified MCP Apps Hook - the main API
13
- export { useMcpApps } from './use-mcp-apps.js';
14
-
15
- // Re-export shared types and client from main entry
16
- export * from '../index.js';
1
+ /**
2
+ * MCP SDK - React Client
3
+ * Simple React hooks for MCP app rendering
4
+ */
5
+
6
+ // Core MCP Hook
7
+ export { useMcp, type UseMcpOptions, type McpClient, type McpConnection } from './use-mcp.js';
8
+
9
+ // App Host (internal use)
10
+ export { useAppHost } from './use-app-host.js';
11
+
12
+ // Simplified MCP Apps Hook - the main API
13
+ export { useMcpApps } from './use-mcp-apps.js';
14
+
15
+ // Re-export shared types and client from main entry
16
+ export * from '../index.js';
@@ -1,214 +1,214 @@
1
- /**
2
- * MCP Apps Hook
3
- *
4
- * Provides utilities for rendering interactive UI components from MCP servers.
5
- */
6
-
7
- import React, { useState, useEffect, useCallback, useRef, memo } from 'react';
8
- import { useAppHost } from './use-app-host.js';
9
- import type { SSEClient } from '../core/sse-client.js';
10
-
11
- export interface McpClient {
12
- connections: Array<{
13
- sessionId: string;
14
- tools: Array<{
15
- name: string;
16
- mcpApp?: {
17
- resourceUri: string;
18
- };
19
- _meta?: {
20
- ui?: {
21
- resourceUri?: string;
22
- };
23
- 'ui/resourceUri'?: string;
24
- };
25
- }>;
26
- }>;
27
- sseClient?: SSEClient | null;
28
- }
29
-
30
- export interface McpAppMetadata {
31
- toolName: string;
32
- resourceUri: string;
33
- sessionId: string;
34
- }
35
-
36
- interface McpAppRendererProps {
37
- metadata: McpAppMetadata;
38
- input?: Record<string, unknown>;
39
- result?: unknown;
40
- status: 'executing' | 'inProgress' | 'complete' | 'idle';
41
- sseClient?: SSEClient | null;
42
- /** Custom CSS class for the container */
43
- className?: string;
44
- }
45
-
46
- /**
47
- * Internal component that renders the MCP app in a sandboxed iframe
48
- */
49
- const McpAppRenderer = memo(function McpAppRenderer({
50
- metadata,
51
- input,
52
- result,
53
- status,
54
- sseClient,
55
- className,
56
- }: McpAppRendererProps) {
57
- const iframeRef = useRef<HTMLIFrameElement>(null);
58
- const { host, error: hostError } = useAppHost(sseClient as SSEClient, iframeRef);
59
- const [isLaunched, setIsLaunched] = useState(false);
60
- const [error, setError] = useState<Error | null>(null);
61
-
62
- // Track which data has been sent to prevent duplicates
63
- const sentInputRef = useRef(false);
64
- const sentResultRef = useRef(false);
65
- const lastInputRef = useRef(input);
66
- const lastResultRef = useRef(result);
67
- const lastStatusRef = useRef(status);
68
-
69
- // Launch the app when host is ready
70
- useEffect(() => {
71
- if (!host || !metadata.resourceUri || !metadata.sessionId) return;
72
-
73
- host
74
- .launch(metadata.resourceUri, metadata.sessionId)
75
- .then(() => setIsLaunched(true))
76
- .catch((err) => setError(err instanceof Error ? err : new Error(String(err))));
77
- }, [host, metadata.resourceUri, metadata.sessionId]);
78
-
79
- // Send tool input when available or when it changes
80
- useEffect(() => {
81
- if (!host || !isLaunched || !input) return;
82
-
83
- // Send if never sent, or if input changed
84
- if (!sentInputRef.current || JSON.stringify(input) !== JSON.stringify(lastInputRef.current)) {
85
- sentInputRef.current = true;
86
- lastInputRef.current = input;
87
- host.sendToolInput(input);
88
- }
89
- }, [host, isLaunched, input]);
90
-
91
- // Send tool result when complete or when it changes
92
- useEffect(() => {
93
- if (!host || !isLaunched || result === undefined) return;
94
- if (status !== 'complete') return;
95
-
96
- // Send if never sent, or if result changed
97
- if (!sentResultRef.current || JSON.stringify(result) !== JSON.stringify(lastResultRef.current)) {
98
- sentResultRef.current = true;
99
- lastResultRef.current = result;
100
- const formattedResult =
101
- typeof result === 'string'
102
- ? { content: [{ type: 'text', text: result }] }
103
- : result;
104
- host.sendToolResult(formattedResult);
105
- }
106
- }, [host, isLaunched, result, status]);
107
-
108
- // Reset sent flags when tool status resets to executing (new tool call)
109
- useEffect(() => {
110
- if (status === 'executing' && lastStatusRef.current !== 'executing') {
111
- sentInputRef.current = false;
112
- sentResultRef.current = false;
113
- }
114
- lastStatusRef.current = status;
115
- }, [status]);
116
-
117
- // Display errors
118
- const displayError = error || hostError;
119
- if (displayError) {
120
- return (
121
- <div className={`p-4 bg-red-900/20 border border-red-700 rounded text-red-200 ${className || ''}`}>
122
- Error: {displayError.message || String(displayError)}
123
- </div>
124
- );
125
- }
126
-
127
- return (
128
- <div className={`w-full border border-gray-700 rounded overflow-hidden bg-white min-h-96 my-2 relative ${className || ''}`}>
129
- <iframe
130
- ref={iframeRef}
131
- sandbox="allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads"
132
- className="w-full h-full min-h-96"
133
- style={{ height: 'auto' }}
134
- title="MCP App"
135
- />
136
- {!isLaunched && (
137
- <div className="absolute inset-0 bg-gray-900/50 flex items-center justify-center pointer-events-none">
138
- <div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
139
- </div>
140
- )}
141
- </div>
142
- );
143
- });
144
-
145
- /**
146
- * Simple hook to get MCP app metadata
147
- *
148
- * @param mcpClient - The MCP client from useMcp() or context
149
- * @returns Object with getAppMetadata function and McpAppRenderer component
150
- *
151
- * @example
152
- * ```tsx
153
- * function ToolRenderer(props) {
154
- * const { getAppMetadata, McpAppRenderer } = useMcpApps(mcpClient);
155
- * const metadata = getAppMetadata(props.name);
156
- *
157
- * if (!metadata) return null;
158
- * return (
159
- * <McpAppRenderer
160
- * metadata={metadata}
161
- * input={props.args}
162
- * result={props.result}
163
- * status={props.status}
164
- * />
165
- * );
166
- * }
167
- * ```
168
- */
169
- export function useMcpApps(mcpClient: McpClient | null) {
170
- /**
171
- * Get MCP app metadata for a tool name
172
- * This is fast and can be called on every render
173
- */
174
- const getAppMetadata = useCallback(
175
- (toolName: string): McpAppMetadata | undefined => {
176
- if (!mcpClient) return undefined;
177
-
178
- const extractedName = extractToolName(toolName);
179
-
180
- for (const conn of mcpClient.connections) {
181
- for (const tool of conn.tools) {
182
- const candidateName = extractToolName(tool.name);
183
- // Check both locations: direct mcpApp or _meta.ui
184
- const resourceUri =
185
- tool.mcpApp?.resourceUri ??
186
- tool._meta?.ui?.resourceUri ??
187
- tool._meta?.['ui/resourceUri'];
188
-
189
- if (resourceUri && candidateName === extractedName) {
190
- return {
191
- toolName: candidateName,
192
- resourceUri,
193
- sessionId: conn.sessionId,
194
- };
195
- }
196
- }
197
- }
198
-
199
- return undefined;
200
- },
201
- [mcpClient]
202
- );
203
-
204
- return { getAppMetadata, McpAppRenderer };
205
- }
206
-
207
- /**
208
- * Extract the base tool name, removing any prefixes
209
- */
210
- function extractToolName(fullName: string): string {
211
- // Handle patterns like "tool_abc123_get-time" -> "get-time"
212
- const match = fullName.match(/(?:tool_[^_]+_)?(.+)$/);
213
- return match?.[1] || fullName;
214
- }
1
+ /**
2
+ * MCP Apps Hook
3
+ *
4
+ * Provides utilities for rendering interactive UI components from MCP servers.
5
+ */
6
+
7
+ import React, { useState, useEffect, useCallback, useRef, memo } from 'react';
8
+ import { useAppHost } from './use-app-host.js';
9
+ import type { SSEClient } from '../core/sse-client.js';
10
+
11
+ export interface McpClient {
12
+ connections: Array<{
13
+ sessionId: string;
14
+ tools: Array<{
15
+ name: string;
16
+ mcpApp?: {
17
+ resourceUri: string;
18
+ };
19
+ _meta?: {
20
+ ui?: {
21
+ resourceUri?: string;
22
+ };
23
+ 'ui/resourceUri'?: string;
24
+ };
25
+ }>;
26
+ }>;
27
+ sseClient?: SSEClient | null;
28
+ }
29
+
30
+ export interface McpAppMetadata {
31
+ toolName: string;
32
+ resourceUri: string;
33
+ sessionId: string;
34
+ }
35
+
36
+ interface McpAppRendererProps {
37
+ metadata: McpAppMetadata;
38
+ input?: Record<string, unknown>;
39
+ result?: unknown;
40
+ status: 'executing' | 'inProgress' | 'complete' | 'idle';
41
+ sseClient?: SSEClient | null;
42
+ /** Custom CSS class for the container */
43
+ className?: string;
44
+ }
45
+
46
+ /**
47
+ * Internal component that renders the MCP app in a sandboxed iframe
48
+ */
49
+ const McpAppRenderer = memo(function McpAppRenderer({
50
+ metadata,
51
+ input,
52
+ result,
53
+ status,
54
+ sseClient,
55
+ className,
56
+ }: McpAppRendererProps) {
57
+ const iframeRef = useRef<HTMLIFrameElement>(null);
58
+ const { host, error: hostError } = useAppHost(sseClient as SSEClient, iframeRef);
59
+ const [isLaunched, setIsLaunched] = useState(false);
60
+ const [error, setError] = useState<Error | null>(null);
61
+
62
+ // Track which data has been sent to prevent duplicates
63
+ const sentInputRef = useRef(false);
64
+ const sentResultRef = useRef(false);
65
+ const lastInputRef = useRef(input);
66
+ const lastResultRef = useRef(result);
67
+ const lastStatusRef = useRef(status);
68
+
69
+ // Launch the app when host is ready
70
+ useEffect(() => {
71
+ if (!host || !metadata.resourceUri || !metadata.sessionId) return;
72
+
73
+ host
74
+ .launch(metadata.resourceUri, metadata.sessionId)
75
+ .then(() => setIsLaunched(true))
76
+ .catch((err) => setError(err instanceof Error ? err : new Error(String(err))));
77
+ }, [host, metadata.resourceUri, metadata.sessionId]);
78
+
79
+ // Send tool input when available or when it changes
80
+ useEffect(() => {
81
+ if (!host || !isLaunched || !input) return;
82
+
83
+ // Send if never sent, or if input changed
84
+ if (!sentInputRef.current || JSON.stringify(input) !== JSON.stringify(lastInputRef.current)) {
85
+ sentInputRef.current = true;
86
+ lastInputRef.current = input;
87
+ host.sendToolInput(input);
88
+ }
89
+ }, [host, isLaunched, input]);
90
+
91
+ // Send tool result when complete or when it changes
92
+ useEffect(() => {
93
+ if (!host || !isLaunched || result === undefined) return;
94
+ if (status !== 'complete') return;
95
+
96
+ // Send if never sent, or if result changed
97
+ if (!sentResultRef.current || JSON.stringify(result) !== JSON.stringify(lastResultRef.current)) {
98
+ sentResultRef.current = true;
99
+ lastResultRef.current = result;
100
+ const formattedResult =
101
+ typeof result === 'string'
102
+ ? { content: [{ type: 'text', text: result }] }
103
+ : result;
104
+ host.sendToolResult(formattedResult);
105
+ }
106
+ }, [host, isLaunched, result, status]);
107
+
108
+ // Reset sent flags when tool status resets to executing (new tool call)
109
+ useEffect(() => {
110
+ if (status === 'executing' && lastStatusRef.current !== 'executing') {
111
+ sentInputRef.current = false;
112
+ sentResultRef.current = false;
113
+ }
114
+ lastStatusRef.current = status;
115
+ }, [status]);
116
+
117
+ // Display errors
118
+ const displayError = error || hostError;
119
+ if (displayError) {
120
+ return (
121
+ <div className={`p-4 bg-red-900/20 border border-red-700 rounded text-red-200 ${className || ''}`}>
122
+ Error: {displayError.message || String(displayError)}
123
+ </div>
124
+ );
125
+ }
126
+
127
+ return (
128
+ <div className={`w-full border border-gray-700 rounded overflow-hidden bg-white min-h-96 my-2 relative ${className || ''}`}>
129
+ <iframe
130
+ ref={iframeRef}
131
+ sandbox="allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads"
132
+ className="w-full h-full min-h-96"
133
+ style={{ height: 'auto' }}
134
+ title="MCP App"
135
+ />
136
+ {!isLaunched && (
137
+ <div className="absolute inset-0 bg-gray-900/50 flex items-center justify-center pointer-events-none">
138
+ <div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
139
+ </div>
140
+ )}
141
+ </div>
142
+ );
143
+ });
144
+
145
+ /**
146
+ * Simple hook to get MCP app metadata
147
+ *
148
+ * @param mcpClient - The MCP client from useMcp() or context
149
+ * @returns Object with getAppMetadata function and McpAppRenderer component
150
+ *
151
+ * @example
152
+ * ```tsx
153
+ * function ToolRenderer(props) {
154
+ * const { getAppMetadata, McpAppRenderer } = useMcpApps(mcpClient);
155
+ * const metadata = getAppMetadata(props.name);
156
+ *
157
+ * if (!metadata) return null;
158
+ * return (
159
+ * <McpAppRenderer
160
+ * metadata={metadata}
161
+ * input={props.args}
162
+ * result={props.result}
163
+ * status={props.status}
164
+ * />
165
+ * );
166
+ * }
167
+ * ```
168
+ */
169
+ export function useMcpApps(mcpClient: McpClient | null) {
170
+ /**
171
+ * Get MCP app metadata for a tool name
172
+ * This is fast and can be called on every render
173
+ */
174
+ const getAppMetadata = useCallback(
175
+ (toolName: string): McpAppMetadata | undefined => {
176
+ if (!mcpClient) return undefined;
177
+
178
+ const extractedName = extractToolName(toolName);
179
+
180
+ for (const conn of mcpClient.connections) {
181
+ for (const tool of conn.tools) {
182
+ const candidateName = extractToolName(tool.name);
183
+ // Check both locations: direct mcpApp or _meta.ui
184
+ const resourceUri =
185
+ tool.mcpApp?.resourceUri ??
186
+ tool._meta?.ui?.resourceUri ??
187
+ tool._meta?.['ui/resourceUri'];
188
+
189
+ if (resourceUri && candidateName === extractedName) {
190
+ return {
191
+ toolName: candidateName,
192
+ resourceUri,
193
+ sessionId: conn.sessionId,
194
+ };
195
+ }
196
+ }
197
+ }
198
+
199
+ return undefined;
200
+ },
201
+ [mcpClient]
202
+ );
203
+
204
+ return { getAppMetadata, McpAppRenderer };
205
+ }
206
+
207
+ /**
208
+ * Extract the base tool name, removing any prefixes
209
+ */
210
+ function extractToolName(fullName: string): string {
211
+ // Handle patterns like "tool_abc123_get-time" -> "get-time"
212
+ const match = fullName.match(/(?:tool_[^_]+_)?(.+)$/);
213
+ return match?.[1] || fullName;
214
+ }
@@ -64,6 +64,13 @@ export interface UseMcpOptions {
64
64
  * @default 60000
65
65
  */
66
66
  requestTimeout?: number;
67
+
68
+ /**
69
+ * Enable client debug logs.
70
+ * @default false
71
+ */
72
+ debug?: boolean;
73
+
67
74
  }
68
75
 
69
76
  export interface McpConnection {
@@ -74,8 +81,9 @@ export interface McpConnection {
74
81
  transport?: string;
75
82
  state: McpConnectionState;
76
83
  tools: ToolInfo[];
84
+ authUrl?: string;
77
85
  error?: string;
78
- connectedAt?: Date;
86
+ createdAt?: Date;
79
87
  }
80
88
 
81
89
  export interface McpClient {
@@ -150,6 +158,11 @@ export interface McpClient {
150
158
  */
151
159
  finishAuth: (sessionId: string, code: string) => Promise<FinishAuthResult>;
152
160
 
161
+ /**
162
+ * Explicitly resume OAuth flow for an existing session
163
+ */
164
+ resumeAuth: (sessionId: string) => Promise<void>;
165
+
153
166
  /**
154
167
  * Call a tool from a session
155
168
  */
@@ -207,6 +220,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
207
220
 
208
221
  const clientRef = useRef<SSEClient | null>(null);
209
222
  const isMountedRef = useRef(true);
223
+ const suppressAuthRedirectSessionsRef = useRef<Set<string>>(new Set());
210
224
 
211
225
  const [connections, setConnections] = useState<McpConnection[]>([]);
212
226
  const [status, setStatus] = useState<'connecting' | 'connected' | 'disconnected' | 'error'>(
@@ -239,7 +253,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
239
253
  setStatus(newStatus);
240
254
  }
241
255
  },
242
- requestTimeout: options.requestTimeout,
256
+ debug: options.debug,
243
257
  };
244
258
 
245
259
  const client = new SSEClient(clientOptions);
@@ -262,25 +276,52 @@ export function useMcp(options: UseMcpOptions): McpClient {
262
276
  /**
263
277
  * Update connections based on event
264
278
  */
265
- const updateConnectionsFromEvent = useCallback((event: McpConnectionEvent) => {
266
- if (!isMountedRef.current) return;
267
-
268
- setConnections((prev: McpConnection[]) => {
269
- switch (event.type) {
270
- case 'state_changed': {
271
- const existing = prev.find((c: McpConnection) => c.sessionId === event.sessionId);
272
- if (existing) {
273
- return prev.map((c: McpConnection) =>
274
- c.sessionId === event.sessionId ? { ...c, state: event.state } : c
279
+ const updateConnectionsFromEvent = useCallback((event: McpConnectionEvent) => {
280
+ if (!isMountedRef.current) return;
281
+
282
+ const isTransientReconnectState = (state: McpConnectionState): boolean =>
283
+ state === 'INITIALIZING' ||
284
+ state === 'VALIDATING' ||
285
+ state === 'RECONNECTING' ||
286
+ state === 'CONNECTING' ||
287
+ state === 'CONNECTED' ||
288
+ state === 'DISCOVERING';
289
+
290
+ setConnections((prev: McpConnection[]) => {
291
+ switch (event.type) {
292
+ case 'state_changed': {
293
+ const existing = prev.find((c: McpConnection) => c.sessionId === event.sessionId);
294
+ if (existing) {
295
+ // In stateless per-request transport, tool calls can emit transient reconnect states.
296
+ // Keep READY sticky to avoid UI flicker from READY -> CONNECTING -> CONNECTED.
297
+ const nextState =
298
+ existing.state === 'READY' && isTransientReconnectState(event.state)
299
+ ? existing.state
300
+ : event.state;
301
+
302
+ return prev.map((c: McpConnection) =>
303
+ c.sessionId === event.sessionId ? {
304
+ ...c,
305
+ state: nextState,
306
+ // update createdAt if present in event, otherwise keep existing
307
+ createdAt: event.createdAt ? new Date(event.createdAt) : c.createdAt
308
+ } : c
275
309
  );
276
310
  } else {
311
+ // Fix: Don't add back disconnected sessions that were just removed
312
+ if (event.state === 'DISCONNECTED') {
313
+ return prev;
314
+ }
315
+
277
316
  return [
278
317
  ...prev,
279
318
  {
280
319
  sessionId: event.sessionId,
281
320
  serverId: event.serverId,
282
321
  serverName: event.serverName,
322
+ serverUrl: event.serverUrl,
283
323
  state: event.state,
324
+ createdAt: event.createdAt ? new Date(event.createdAt) : undefined,
284
325
  tools: [],
285
326
  },
286
327
  ];
@@ -303,14 +344,17 @@ export function useMcp(options: UseMcpOptions): McpClient {
303
344
  if (event.authUrl) {
304
345
  onLog?.('info', `OAuth required - redirecting to ${event.authUrl}`, { authUrl: event.authUrl });
305
346
 
306
- if (onRedirect) {
307
- onRedirect(event.authUrl);
308
- } else if (typeof window !== 'undefined') {
309
- window.location.href = event.authUrl;
347
+ // Suppress redirects/popups for auto-restore on page load.
348
+ if (!suppressAuthRedirectSessionsRef.current.has(event.sessionId)) {
349
+ if (onRedirect) {
350
+ onRedirect(event.authUrl);
351
+ } else if (typeof window !== 'undefined') {
352
+ window.location.href = event.authUrl;
353
+ }
310
354
  }
311
355
  }
312
356
  return prev.map((c: McpConnection) =>
313
- c.sessionId === event.sessionId ? { ...c, state: 'AUTHENTICATING' } : c
357
+ c.sessionId === event.sessionId ? { ...c, state: 'AUTHENTICATING', authUrl: event.authUrl } : c
314
358
  );
315
359
  }
316
360
 
@@ -328,7 +372,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
328
372
  return prev;
329
373
  }
330
374
  });
331
- }, [onLog]);
375
+ }, [onLog, onRedirect]);
332
376
 
333
377
  /**
334
378
  * Load sessions from server
@@ -351,7 +395,8 @@ export function useMcp(options: UseMcpOptions): McpClient {
351
395
  serverName: s.serverName ?? 'Unknown Server',
352
396
  serverUrl: s.serverUrl,
353
397
  transport: s.transport,
354
- state: 'VALIDATING' as McpConnectionState,
398
+ state: (s.active === false ? 'AUTHENTICATING' : 'VALIDATING') as McpConnectionState,
399
+ createdAt: new Date(s.createdAt),
355
400
  tools: [],
356
401
  }))
357
402
  );
@@ -362,9 +407,16 @@ export function useMcp(options: UseMcpOptions): McpClient {
362
407
  sessions.map(async (session: SessionInfo) => {
363
408
  if (clientRef.current) {
364
409
  try {
410
+ // Pending auth sessions should not auto-trigger popup/redirect on reload.
411
+ if (session.active === false) {
412
+ return;
413
+ }
414
+ suppressAuthRedirectSessionsRef.current.add(session.sessionId);
365
415
  await clientRef.current.restoreSession(session.sessionId);
366
416
  } catch (error) {
367
417
  console.error(`[useMcp] Failed to validate session ${session.sessionId}:`, error);
418
+ } finally {
419
+ suppressAuthRedirectSessionsRef.current.delete(session.sessionId);
368
420
  }
369
421
  }
370
422
  })
@@ -448,6 +500,18 @@ export function useMcp(options: UseMcpOptions): McpClient {
448
500
  return await clientRef.current.finishAuth(sessionId, code);
449
501
  }, []);
450
502
 
503
+ /**
504
+ * Explicit user action to resume OAuth for an existing pending session.
505
+ */
506
+ const resumeAuth = useCallback(async (sessionId: string): Promise<void> => {
507
+ if (!clientRef.current) {
508
+ throw new Error('SSE client not initialized');
509
+ }
510
+ // Ensure this attempt is not suppressed as background restore.
511
+ suppressAuthRedirectSessionsRef.current.delete(sessionId);
512
+ await clientRef.current.restoreSession(sessionId);
513
+ }, []);
514
+
451
515
  /**
452
516
  * Call a tool
453
517
  */
@@ -565,6 +629,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
565
629
  connectSSE,
566
630
  disconnectSSE,
567
631
  finishAuth,
632
+ resumeAuth,
568
633
  callTool,
569
634
  listTools,
570
635
  listPrompts,