@mcp-ts/sdk 1.3.7 → 1.3.9
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/LICENSE +21 -21
- package/README.md +398 -404
- package/dist/adapters/agui-middleware.js.map +1 -1
- package/dist/adapters/agui-middleware.mjs.map +1 -1
- package/dist/bin/mcp-ts.js +0 -0
- package/dist/bin/mcp-ts.js.map +1 -1
- package/dist/bin/mcp-ts.mjs +0 -0
- package/dist/bin/mcp-ts.mjs.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +2 -2
- package/dist/client/react.d.ts +2 -2
- package/dist/client/react.js +25 -2
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +26 -3
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs.map +1 -1
- package/package.json +185 -185
- package/src/adapters/agui-middleware.ts +382 -382
- package/src/bin/mcp-ts.ts +102 -102
- package/src/client/core/app-host.ts +417 -417
- package/src/client/core/sse-client.ts +371 -371
- package/src/client/core/types.ts +31 -31
- package/src/client/index.ts +27 -27
- package/src/client/react/index.ts +16 -16
- package/src/client/react/use-app-host.ts +73 -73
- package/src/client/react/use-mcp-apps.tsx +247 -214
- package/src/client/react/use-mcp.ts +641 -641
- package/src/client/vue/index.ts +10 -10
- package/src/client/vue/use-mcp.ts +617 -617
- package/src/index.ts +11 -11
- package/src/server/handlers/nextjs-handler.ts +204 -204
- package/src/server/handlers/sse-handler.ts +631 -631
- package/src/server/index.ts +57 -57
- package/src/server/mcp/multi-session-client.ts +228 -228
- package/src/server/mcp/oauth-client.ts +1188 -1188
- package/src/server/mcp/storage-oauth-provider.ts +272 -272
- package/src/server/storage/file-backend.ts +157 -157
- package/src/server/storage/index.ts +176 -176
- package/src/server/storage/memory-backend.ts +123 -123
- package/src/server/storage/redis-backend.ts +276 -276
- package/src/server/storage/redis.ts +160 -160
- package/src/server/storage/sqlite-backend.ts +182 -182
- package/src/server/storage/supabase-backend.ts +228 -228
- package/src/server/storage/types.ts +116 -116
- package/src/shared/constants.ts +29 -29
- package/src/shared/errors.ts +133 -133
- package/src/shared/event-routing.ts +28 -28
- package/src/shared/events.ts +180 -180
- package/src/shared/index.ts +75 -75
- package/src/shared/tool-utils.ts +61 -61
- package/src/shared/types.ts +282 -282
- package/src/shared/utils.ts +38 -38
- package/supabase/migrations/20260330195700_install_mcp_sessions.sql +84 -84
|
@@ -1,641 +1,641 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useMcp React Hook
|
|
3
|
-
* Manages MCP connections with SSE-based real-time updates
|
|
4
|
-
* Based on Cloudflare's agents pattern
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
8
|
-
import { SSEClient, type SSEClientOptions } from '../core/sse-client';
|
|
9
|
-
import type { McpConnectionEvent, McpConnectionState } from '../../shared/events';
|
|
10
|
-
import type {
|
|
11
|
-
ToolInfo,
|
|
12
|
-
FinishAuthResult,
|
|
13
|
-
ListToolsRpcResult,
|
|
14
|
-
ListPromptsResult,
|
|
15
|
-
ListResourcesResult,
|
|
16
|
-
SessionInfo,
|
|
17
|
-
} from '../../shared/types';
|
|
18
|
-
|
|
19
|
-
export interface UseMcpOptions {
|
|
20
|
-
/**
|
|
21
|
-
* SSE endpoint URL
|
|
22
|
-
*/
|
|
23
|
-
url: string;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* User/Client identifier
|
|
27
|
-
*/
|
|
28
|
-
identity: string;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Optional auth token
|
|
32
|
-
*/
|
|
33
|
-
authToken?: string;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Auto-connect on mount
|
|
37
|
-
* @default true
|
|
38
|
-
*/
|
|
39
|
-
autoConnect?: boolean;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Auto-initialize sessions on mount
|
|
43
|
-
* @default true
|
|
44
|
-
*/
|
|
45
|
-
autoInitialize?: boolean;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Connection event callback
|
|
49
|
-
*/
|
|
50
|
-
onConnectionEvent?: (event: McpConnectionEvent) => void;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Debug logging callback
|
|
54
|
-
*/
|
|
55
|
-
onLog?: (level: string, message: string, metadata?: Record<string, unknown>) => void;
|
|
56
|
-
/**
|
|
57
|
-
* Optional callback to handle OAuth redirects (e.g. for popup flow)
|
|
58
|
-
* If provided, this will be called instead of window.location.href assignment
|
|
59
|
-
*/
|
|
60
|
-
onRedirect?: (url: string) => void;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Request timeout in milliseconds
|
|
64
|
-
* @default 60000
|
|
65
|
-
*/
|
|
66
|
-
requestTimeout?: number;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Enable client debug logs.
|
|
70
|
-
* @default false
|
|
71
|
-
*/
|
|
72
|
-
debug?: boolean;
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface McpConnection {
|
|
77
|
-
sessionId: string;
|
|
78
|
-
serverId: string;
|
|
79
|
-
serverName: string;
|
|
80
|
-
serverUrl?: string;
|
|
81
|
-
transport?: string;
|
|
82
|
-
state: McpConnectionState;
|
|
83
|
-
tools: ToolInfo[];
|
|
84
|
-
authUrl?: string;
|
|
85
|
-
error?: string;
|
|
86
|
-
createdAt?: Date;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export interface McpClient {
|
|
90
|
-
/**
|
|
91
|
-
* All connections
|
|
92
|
-
*/
|
|
93
|
-
connections: McpConnection[];
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* SSE connection status
|
|
97
|
-
*/
|
|
98
|
-
status: 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Whether initializing
|
|
102
|
-
*/
|
|
103
|
-
isInitializing: boolean;
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Connect to an MCP server
|
|
107
|
-
*/
|
|
108
|
-
connect: (params: {
|
|
109
|
-
serverId: string;
|
|
110
|
-
serverName: string;
|
|
111
|
-
serverUrl: string;
|
|
112
|
-
callbackUrl: string;
|
|
113
|
-
transportType?: 'sse' | 'streamable_http';
|
|
114
|
-
}) => Promise<string>;
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Disconnect from an MCP server
|
|
118
|
-
*/
|
|
119
|
-
disconnect: (sessionId: string) => Promise<void>;
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Get connection by session ID
|
|
123
|
-
*/
|
|
124
|
-
getConnection: (sessionId: string) => McpConnection | undefined;
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Get connection by server ID
|
|
128
|
-
*/
|
|
129
|
-
getConnectionByServerId: (serverId: string) => McpConnection | undefined;
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Check if server is connected
|
|
133
|
-
*/
|
|
134
|
-
isServerConnected: (serverId: string) => boolean;
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Get tools for a session
|
|
138
|
-
*/
|
|
139
|
-
getTools: (sessionId: string) => ToolInfo[];
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Refresh all connections
|
|
143
|
-
*/
|
|
144
|
-
refresh: () => Promise<void>;
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Manually connect SSE
|
|
148
|
-
*/
|
|
149
|
-
connectSSE: () => void;
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Manually disconnect SSE
|
|
153
|
-
*/
|
|
154
|
-
disconnectSSE: () => void;
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Complete OAuth authorization
|
|
158
|
-
*/
|
|
159
|
-
finishAuth: (sessionId: string, code: string) => Promise<FinishAuthResult>;
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Explicitly resume OAuth flow for an existing session
|
|
163
|
-
*/
|
|
164
|
-
resumeAuth: (sessionId: string) => Promise<void>;
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Call a tool from a session
|
|
168
|
-
*/
|
|
169
|
-
callTool: (
|
|
170
|
-
sessionId: string,
|
|
171
|
-
toolName: string,
|
|
172
|
-
toolArgs: Record<string, unknown>
|
|
173
|
-
) => Promise<unknown>;
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* List available tools for a session
|
|
177
|
-
*/
|
|
178
|
-
listTools: (sessionId: string) => Promise<ListToolsRpcResult>;
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* List available prompts for a session
|
|
182
|
-
*/
|
|
183
|
-
listPrompts: (sessionId: string) => Promise<ListPromptsResult>;
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Get a specific prompt with arguments
|
|
187
|
-
*/
|
|
188
|
-
getPrompt: (sessionId: string, name: string, args?: Record<string, string>) => Promise<unknown>;
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* List available resources for a session
|
|
192
|
-
*/
|
|
193
|
-
listResources: (sessionId: string) => Promise<ListResourcesResult>;
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Read a specific resource
|
|
197
|
-
*/
|
|
198
|
-
readResource: (sessionId: string, uri: string) => Promise<unknown>;
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Access the underlying SSEClient instance (for advanced usage like AppHost)
|
|
202
|
-
*/
|
|
203
|
-
sseClient: SSEClient | null;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* React hook for MCP connection management with SSE
|
|
208
|
-
*/
|
|
209
|
-
export function useMcp(options: UseMcpOptions): McpClient {
|
|
210
|
-
const {
|
|
211
|
-
url,
|
|
212
|
-
identity,
|
|
213
|
-
authToken,
|
|
214
|
-
autoConnect = true,
|
|
215
|
-
autoInitialize = true,
|
|
216
|
-
onConnectionEvent,
|
|
217
|
-
onLog,
|
|
218
|
-
onRedirect,
|
|
219
|
-
} = options;
|
|
220
|
-
|
|
221
|
-
const clientRef = useRef<SSEClient | null>(null);
|
|
222
|
-
const isMountedRef = useRef(true);
|
|
223
|
-
const suppressAuthRedirectSessionsRef = useRef<Set<string>>(new Set());
|
|
224
|
-
|
|
225
|
-
const [connections, setConnections] = useState<McpConnection[]>([]);
|
|
226
|
-
const [status, setStatus] = useState<'connecting' | 'connected' | 'disconnected' | 'error'>(
|
|
227
|
-
'disconnected'
|
|
228
|
-
);
|
|
229
|
-
const [isInitializing, setIsInitializing] = useState(false);
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Initialize SSE client
|
|
233
|
-
*/
|
|
234
|
-
useEffect(() => {
|
|
235
|
-
isMountedRef.current = true;
|
|
236
|
-
|
|
237
|
-
const clientOptions: SSEClientOptions = {
|
|
238
|
-
url,
|
|
239
|
-
identity,
|
|
240
|
-
authToken,
|
|
241
|
-
onConnectionEvent: (event) => {
|
|
242
|
-
// Update local state based on event
|
|
243
|
-
updateConnectionsFromEvent(event);
|
|
244
|
-
|
|
245
|
-
// Call user callback
|
|
246
|
-
onConnectionEvent?.(event);
|
|
247
|
-
},
|
|
248
|
-
onObservabilityEvent: (event) => {
|
|
249
|
-
onLog?.(event.level || 'info', event.message || event.displayMessage || 'No message', event.metadata);
|
|
250
|
-
},
|
|
251
|
-
onStatusChange: (newStatus) => {
|
|
252
|
-
if (isMountedRef.current) {
|
|
253
|
-
setStatus(newStatus);
|
|
254
|
-
}
|
|
255
|
-
},
|
|
256
|
-
debug: options.debug,
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
const client = new SSEClient(clientOptions);
|
|
260
|
-
clientRef.current = client;
|
|
261
|
-
|
|
262
|
-
if (autoConnect) {
|
|
263
|
-
client.connect();
|
|
264
|
-
|
|
265
|
-
if (autoInitialize) {
|
|
266
|
-
loadSessions();
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return () => {
|
|
271
|
-
isMountedRef.current = false;
|
|
272
|
-
client.disconnect();
|
|
273
|
-
};
|
|
274
|
-
}, [url, identity, authToken, autoConnect, autoInitialize]);
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Update connections based on event
|
|
278
|
-
*/
|
|
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
|
|
309
|
-
);
|
|
310
|
-
} else {
|
|
311
|
-
// Fix: Don't add back disconnected sessions that were just removed
|
|
312
|
-
if (event.state === 'DISCONNECTED') {
|
|
313
|
-
return prev;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return [
|
|
317
|
-
...prev,
|
|
318
|
-
{
|
|
319
|
-
sessionId: event.sessionId,
|
|
320
|
-
serverId: event.serverId,
|
|
321
|
-
serverName: event.serverName,
|
|
322
|
-
serverUrl: event.serverUrl,
|
|
323
|
-
state: event.state,
|
|
324
|
-
createdAt: event.createdAt ? new Date(event.createdAt) : undefined,
|
|
325
|
-
tools: [],
|
|
326
|
-
},
|
|
327
|
-
];
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
case 'tools_discovered': {
|
|
332
|
-
// Preload UI resources for instant loading when tools are discovered
|
|
333
|
-
if (clientRef.current && event.tools?.length) {
|
|
334
|
-
clientRef.current.preloadToolUiResources(event.sessionId, event.tools);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return prev.map((c: McpConnection) =>
|
|
338
|
-
c.sessionId === event.sessionId ? { ...c, tools: event.tools, state: 'READY' } : c
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
case 'auth_required': {
|
|
343
|
-
// Handle OAuth redirect
|
|
344
|
-
if (event.authUrl) {
|
|
345
|
-
onLog?.('info', `OAuth required - redirecting to ${event.authUrl}`, { authUrl: event.authUrl });
|
|
346
|
-
|
|
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
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
return prev.map((c: McpConnection) =>
|
|
357
|
-
c.sessionId === event.sessionId ? { ...c, state: 'AUTHENTICATING', authUrl: event.authUrl } : c
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
case 'error': {
|
|
362
|
-
return prev.map((c: McpConnection) =>
|
|
363
|
-
c.sessionId === event.sessionId ? { ...c, state: 'FAILED', error: event.error } : c
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
case 'disconnected': {
|
|
368
|
-
return prev.filter((c: McpConnection) => c.sessionId !== event.sessionId);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
default:
|
|
372
|
-
return prev;
|
|
373
|
-
}
|
|
374
|
-
});
|
|
375
|
-
}, [onLog, onRedirect]);
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Load sessions from server
|
|
379
|
-
*/
|
|
380
|
-
const loadSessions = useCallback(async () => {
|
|
381
|
-
if (!clientRef.current) return;
|
|
382
|
-
|
|
383
|
-
try {
|
|
384
|
-
setIsInitializing(true);
|
|
385
|
-
|
|
386
|
-
const result = await clientRef.current.getSessions();
|
|
387
|
-
const sessions = result.sessions || [];
|
|
388
|
-
|
|
389
|
-
// Initialize connections
|
|
390
|
-
if (isMountedRef.current) {
|
|
391
|
-
setConnections(
|
|
392
|
-
sessions.map((s: SessionInfo) => ({
|
|
393
|
-
sessionId: s.sessionId,
|
|
394
|
-
serverId: s.serverId ?? 'unknown',
|
|
395
|
-
serverName: s.serverName ?? 'Unknown Server',
|
|
396
|
-
serverUrl: s.serverUrl,
|
|
397
|
-
transport: s.transport,
|
|
398
|
-
state: (s.active === false ? 'AUTHENTICATING' : 'VALIDATING') as McpConnectionState,
|
|
399
|
-
createdAt: new Date(s.createdAt),
|
|
400
|
-
tools: [],
|
|
401
|
-
}))
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Validate each session in parallel
|
|
406
|
-
await Promise.all(
|
|
407
|
-
sessions.map(async (session: SessionInfo) => {
|
|
408
|
-
if (clientRef.current) {
|
|
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);
|
|
415
|
-
await clientRef.current.restoreSession(session.sessionId);
|
|
416
|
-
} catch (error) {
|
|
417
|
-
console.error(`[useMcp] Failed to validate session ${session.sessionId}:`, error);
|
|
418
|
-
} finally {
|
|
419
|
-
suppressAuthRedirectSessionsRef.current.delete(session.sessionId);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
})
|
|
423
|
-
);
|
|
424
|
-
} catch (error) {
|
|
425
|
-
console.error('[useMcp] Failed to load sessions:', error);
|
|
426
|
-
onLog?.('error', 'Failed to load sessions', { error });
|
|
427
|
-
} finally {
|
|
428
|
-
if (isMountedRef.current) {
|
|
429
|
-
setIsInitializing(false);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}, [onLog]);
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Connect to an MCP server
|
|
436
|
-
*/
|
|
437
|
-
const connect = useCallback(
|
|
438
|
-
async (params: {
|
|
439
|
-
serverId: string;
|
|
440
|
-
serverName: string;
|
|
441
|
-
serverUrl: string;
|
|
442
|
-
callbackUrl: string;
|
|
443
|
-
transportType?: 'sse' | 'streamable_http';
|
|
444
|
-
}): Promise<string> => {
|
|
445
|
-
if (!clientRef.current) {
|
|
446
|
-
throw new Error('SSE client not initialized');
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const result = await clientRef.current.connectToServer(params);
|
|
450
|
-
return result.sessionId;
|
|
451
|
-
},
|
|
452
|
-
[]
|
|
453
|
-
);
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Disconnect from an MCP server
|
|
457
|
-
*/
|
|
458
|
-
const disconnect = useCallback(async (sessionId: string): Promise<void> => {
|
|
459
|
-
if (!clientRef.current) {
|
|
460
|
-
throw new Error('SSE client not initialized');
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
await clientRef.current.disconnectFromServer(sessionId);
|
|
464
|
-
|
|
465
|
-
// Remove from local state
|
|
466
|
-
if (isMountedRef.current) {
|
|
467
|
-
setConnections((prev: McpConnection[]) => prev.filter((c: McpConnection) => c.sessionId !== sessionId));
|
|
468
|
-
}
|
|
469
|
-
}, []);
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Refresh all connections
|
|
473
|
-
*/
|
|
474
|
-
const refresh = useCallback(async () => {
|
|
475
|
-
await loadSessions();
|
|
476
|
-
}, [loadSessions]);
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
* Manually connect SSE
|
|
480
|
-
*/
|
|
481
|
-
const connectSSE = useCallback(() => {
|
|
482
|
-
clientRef.current?.connect();
|
|
483
|
-
}, []);
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Manually disconnect SSE
|
|
487
|
-
*/
|
|
488
|
-
const disconnectSSE = useCallback(() => {
|
|
489
|
-
clientRef.current?.disconnect();
|
|
490
|
-
}, []);
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* Complete OAuth authorization
|
|
494
|
-
*/
|
|
495
|
-
const finishAuth = useCallback(async (sessionId: string, code: string): Promise<FinishAuthResult> => {
|
|
496
|
-
if (!clientRef.current) {
|
|
497
|
-
throw new Error('SSE client not initialized');
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
return await clientRef.current.finishAuth(sessionId, code);
|
|
501
|
-
}, []);
|
|
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
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Call a tool
|
|
517
|
-
*/
|
|
518
|
-
const callTool = useCallback(
|
|
519
|
-
async (
|
|
520
|
-
sessionId: string,
|
|
521
|
-
toolName: string,
|
|
522
|
-
toolArgs: Record<string, unknown>
|
|
523
|
-
): Promise<unknown> => {
|
|
524
|
-
if (!clientRef.current) {
|
|
525
|
-
throw new Error('SSE client not initialized');
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
return await clientRef.current.callTool(sessionId, toolName, toolArgs);
|
|
529
|
-
},
|
|
530
|
-
[]
|
|
531
|
-
);
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* List tools (refresh tool list)
|
|
535
|
-
*/
|
|
536
|
-
const listTools = useCallback(async (sessionId: string): Promise<ListToolsRpcResult> => {
|
|
537
|
-
if (!clientRef.current) {
|
|
538
|
-
throw new Error('SSE client not initialized');
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
return await clientRef.current.listTools(sessionId);
|
|
542
|
-
}, []);
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* List prompts
|
|
546
|
-
*/
|
|
547
|
-
const listPrompts = useCallback(async (sessionId: string): Promise<ListPromptsResult> => {
|
|
548
|
-
if (!clientRef.current) {
|
|
549
|
-
throw new Error('SSE client not initialized');
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
return await clientRef.current.listPrompts(sessionId);
|
|
553
|
-
}, []);
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* Get a specific prompt
|
|
557
|
-
*/
|
|
558
|
-
const getPrompt = useCallback(
|
|
559
|
-
async (sessionId: string, name: string, args?: Record<string, string>): Promise<unknown> => {
|
|
560
|
-
if (!clientRef.current) {
|
|
561
|
-
throw new Error('SSE client not initialized');
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
return await clientRef.current.getPrompt(sessionId, name, args);
|
|
565
|
-
},
|
|
566
|
-
[]
|
|
567
|
-
);
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* List resources
|
|
571
|
-
*/
|
|
572
|
-
const listResources = useCallback(async (sessionId: string): Promise<ListResourcesResult> => {
|
|
573
|
-
if (!clientRef.current) {
|
|
574
|
-
throw new Error('SSE client not initialized');
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
return await clientRef.current.listResources(sessionId);
|
|
578
|
-
}, []);
|
|
579
|
-
|
|
580
|
-
/**
|
|
581
|
-
* Read a specific resource
|
|
582
|
-
*/
|
|
583
|
-
const readResource = useCallback(async (sessionId: string, uri: string): Promise<unknown> => {
|
|
584
|
-
if (!clientRef.current) {
|
|
585
|
-
throw new Error('SSE client not initialized');
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
return await clientRef.current.readResource(sessionId, uri);
|
|
589
|
-
}, []);
|
|
590
|
-
|
|
591
|
-
// Utility functions
|
|
592
|
-
const getConnection = useCallback(
|
|
593
|
-
(sessionId: string) => connections.find((c: McpConnection) => c.sessionId === sessionId),
|
|
594
|
-
[connections]
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
const getConnectionByServerId = useCallback(
|
|
598
|
-
(serverId: string) => connections.find((c: McpConnection) => c.serverId === serverId),
|
|
599
|
-
[connections]
|
|
600
|
-
);
|
|
601
|
-
|
|
602
|
-
const isServerConnected = useCallback(
|
|
603
|
-
(serverId: string) => {
|
|
604
|
-
const conn = getConnectionByServerId(serverId);
|
|
605
|
-
return conn ? conn.state === 'CONNECTED' || conn.state === 'DISCOVERING' || conn.state === 'READY' : false;
|
|
606
|
-
},
|
|
607
|
-
[getConnectionByServerId]
|
|
608
|
-
);
|
|
609
|
-
|
|
610
|
-
const getTools = useCallback(
|
|
611
|
-
(sessionId: string) => {
|
|
612
|
-
const conn = getConnection(sessionId);
|
|
613
|
-
return conn?.tools || [];
|
|
614
|
-
},
|
|
615
|
-
[getConnection]
|
|
616
|
-
);
|
|
617
|
-
|
|
618
|
-
return {
|
|
619
|
-
connections,
|
|
620
|
-
status,
|
|
621
|
-
isInitializing,
|
|
622
|
-
connect,
|
|
623
|
-
disconnect,
|
|
624
|
-
getConnection,
|
|
625
|
-
getConnectionByServerId,
|
|
626
|
-
isServerConnected,
|
|
627
|
-
getTools,
|
|
628
|
-
refresh,
|
|
629
|
-
connectSSE,
|
|
630
|
-
disconnectSSE,
|
|
631
|
-
finishAuth,
|
|
632
|
-
resumeAuth,
|
|
633
|
-
callTool,
|
|
634
|
-
listTools,
|
|
635
|
-
listPrompts,
|
|
636
|
-
getPrompt,
|
|
637
|
-
listResources,
|
|
638
|
-
readResource,
|
|
639
|
-
sseClient: clientRef.current,
|
|
640
|
-
};
|
|
641
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* useMcp React Hook
|
|
3
|
+
* Manages MCP connections with SSE-based real-time updates
|
|
4
|
+
* Based on Cloudflare's agents pattern
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
8
|
+
import { SSEClient, type SSEClientOptions } from '../core/sse-client';
|
|
9
|
+
import type { McpConnectionEvent, McpConnectionState } from '../../shared/events';
|
|
10
|
+
import type {
|
|
11
|
+
ToolInfo,
|
|
12
|
+
FinishAuthResult,
|
|
13
|
+
ListToolsRpcResult,
|
|
14
|
+
ListPromptsResult,
|
|
15
|
+
ListResourcesResult,
|
|
16
|
+
SessionInfo,
|
|
17
|
+
} from '../../shared/types';
|
|
18
|
+
|
|
19
|
+
export interface UseMcpOptions {
|
|
20
|
+
/**
|
|
21
|
+
* SSE endpoint URL
|
|
22
|
+
*/
|
|
23
|
+
url: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* User/Client identifier
|
|
27
|
+
*/
|
|
28
|
+
identity: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Optional auth token
|
|
32
|
+
*/
|
|
33
|
+
authToken?: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Auto-connect on mount
|
|
37
|
+
* @default true
|
|
38
|
+
*/
|
|
39
|
+
autoConnect?: boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Auto-initialize sessions on mount
|
|
43
|
+
* @default true
|
|
44
|
+
*/
|
|
45
|
+
autoInitialize?: boolean;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Connection event callback
|
|
49
|
+
*/
|
|
50
|
+
onConnectionEvent?: (event: McpConnectionEvent) => void;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Debug logging callback
|
|
54
|
+
*/
|
|
55
|
+
onLog?: (level: string, message: string, metadata?: Record<string, unknown>) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Optional callback to handle OAuth redirects (e.g. for popup flow)
|
|
58
|
+
* If provided, this will be called instead of window.location.href assignment
|
|
59
|
+
*/
|
|
60
|
+
onRedirect?: (url: string) => void;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Request timeout in milliseconds
|
|
64
|
+
* @default 60000
|
|
65
|
+
*/
|
|
66
|
+
requestTimeout?: number;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Enable client debug logs.
|
|
70
|
+
* @default false
|
|
71
|
+
*/
|
|
72
|
+
debug?: boolean;
|
|
73
|
+
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface McpConnection {
|
|
77
|
+
sessionId: string;
|
|
78
|
+
serverId: string;
|
|
79
|
+
serverName: string;
|
|
80
|
+
serverUrl?: string;
|
|
81
|
+
transport?: string;
|
|
82
|
+
state: McpConnectionState;
|
|
83
|
+
tools: ToolInfo[];
|
|
84
|
+
authUrl?: string;
|
|
85
|
+
error?: string;
|
|
86
|
+
createdAt?: Date;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface McpClient {
|
|
90
|
+
/**
|
|
91
|
+
* All connections
|
|
92
|
+
*/
|
|
93
|
+
connections: McpConnection[];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* SSE connection status
|
|
97
|
+
*/
|
|
98
|
+
status: 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Whether initializing
|
|
102
|
+
*/
|
|
103
|
+
isInitializing: boolean;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Connect to an MCP server
|
|
107
|
+
*/
|
|
108
|
+
connect: (params: {
|
|
109
|
+
serverId: string;
|
|
110
|
+
serverName: string;
|
|
111
|
+
serverUrl: string;
|
|
112
|
+
callbackUrl: string;
|
|
113
|
+
transportType?: 'sse' | 'streamable_http';
|
|
114
|
+
}) => Promise<string>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Disconnect from an MCP server
|
|
118
|
+
*/
|
|
119
|
+
disconnect: (sessionId: string) => Promise<void>;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get connection by session ID
|
|
123
|
+
*/
|
|
124
|
+
getConnection: (sessionId: string) => McpConnection | undefined;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get connection by server ID
|
|
128
|
+
*/
|
|
129
|
+
getConnectionByServerId: (serverId: string) => McpConnection | undefined;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if server is connected
|
|
133
|
+
*/
|
|
134
|
+
isServerConnected: (serverId: string) => boolean;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get tools for a session
|
|
138
|
+
*/
|
|
139
|
+
getTools: (sessionId: string) => ToolInfo[];
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Refresh all connections
|
|
143
|
+
*/
|
|
144
|
+
refresh: () => Promise<void>;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Manually connect SSE
|
|
148
|
+
*/
|
|
149
|
+
connectSSE: () => void;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Manually disconnect SSE
|
|
153
|
+
*/
|
|
154
|
+
disconnectSSE: () => void;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Complete OAuth authorization
|
|
158
|
+
*/
|
|
159
|
+
finishAuth: (sessionId: string, code: string) => Promise<FinishAuthResult>;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Explicitly resume OAuth flow for an existing session
|
|
163
|
+
*/
|
|
164
|
+
resumeAuth: (sessionId: string) => Promise<void>;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Call a tool from a session
|
|
168
|
+
*/
|
|
169
|
+
callTool: (
|
|
170
|
+
sessionId: string,
|
|
171
|
+
toolName: string,
|
|
172
|
+
toolArgs: Record<string, unknown>
|
|
173
|
+
) => Promise<unknown>;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* List available tools for a session
|
|
177
|
+
*/
|
|
178
|
+
listTools: (sessionId: string) => Promise<ListToolsRpcResult>;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* List available prompts for a session
|
|
182
|
+
*/
|
|
183
|
+
listPrompts: (sessionId: string) => Promise<ListPromptsResult>;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get a specific prompt with arguments
|
|
187
|
+
*/
|
|
188
|
+
getPrompt: (sessionId: string, name: string, args?: Record<string, string>) => Promise<unknown>;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* List available resources for a session
|
|
192
|
+
*/
|
|
193
|
+
listResources: (sessionId: string) => Promise<ListResourcesResult>;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Read a specific resource
|
|
197
|
+
*/
|
|
198
|
+
readResource: (sessionId: string, uri: string) => Promise<unknown>;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Access the underlying SSEClient instance (for advanced usage like AppHost)
|
|
202
|
+
*/
|
|
203
|
+
sseClient: SSEClient | null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* React hook for MCP connection management with SSE
|
|
208
|
+
*/
|
|
209
|
+
export function useMcp(options: UseMcpOptions): McpClient {
|
|
210
|
+
const {
|
|
211
|
+
url,
|
|
212
|
+
identity,
|
|
213
|
+
authToken,
|
|
214
|
+
autoConnect = true,
|
|
215
|
+
autoInitialize = true,
|
|
216
|
+
onConnectionEvent,
|
|
217
|
+
onLog,
|
|
218
|
+
onRedirect,
|
|
219
|
+
} = options;
|
|
220
|
+
|
|
221
|
+
const clientRef = useRef<SSEClient | null>(null);
|
|
222
|
+
const isMountedRef = useRef(true);
|
|
223
|
+
const suppressAuthRedirectSessionsRef = useRef<Set<string>>(new Set());
|
|
224
|
+
|
|
225
|
+
const [connections, setConnections] = useState<McpConnection[]>([]);
|
|
226
|
+
const [status, setStatus] = useState<'connecting' | 'connected' | 'disconnected' | 'error'>(
|
|
227
|
+
'disconnected'
|
|
228
|
+
);
|
|
229
|
+
const [isInitializing, setIsInitializing] = useState(false);
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Initialize SSE client
|
|
233
|
+
*/
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
isMountedRef.current = true;
|
|
236
|
+
|
|
237
|
+
const clientOptions: SSEClientOptions = {
|
|
238
|
+
url,
|
|
239
|
+
identity,
|
|
240
|
+
authToken,
|
|
241
|
+
onConnectionEvent: (event) => {
|
|
242
|
+
// Update local state based on event
|
|
243
|
+
updateConnectionsFromEvent(event);
|
|
244
|
+
|
|
245
|
+
// Call user callback
|
|
246
|
+
onConnectionEvent?.(event);
|
|
247
|
+
},
|
|
248
|
+
onObservabilityEvent: (event) => {
|
|
249
|
+
onLog?.(event.level || 'info', event.message || event.displayMessage || 'No message', event.metadata);
|
|
250
|
+
},
|
|
251
|
+
onStatusChange: (newStatus) => {
|
|
252
|
+
if (isMountedRef.current) {
|
|
253
|
+
setStatus(newStatus);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
debug: options.debug,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const client = new SSEClient(clientOptions);
|
|
260
|
+
clientRef.current = client;
|
|
261
|
+
|
|
262
|
+
if (autoConnect) {
|
|
263
|
+
client.connect();
|
|
264
|
+
|
|
265
|
+
if (autoInitialize) {
|
|
266
|
+
loadSessions();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return () => {
|
|
271
|
+
isMountedRef.current = false;
|
|
272
|
+
client.disconnect();
|
|
273
|
+
};
|
|
274
|
+
}, [url, identity, authToken, autoConnect, autoInitialize]);
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Update connections based on event
|
|
278
|
+
*/
|
|
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
|
|
309
|
+
);
|
|
310
|
+
} else {
|
|
311
|
+
// Fix: Don't add back disconnected sessions that were just removed
|
|
312
|
+
if (event.state === 'DISCONNECTED') {
|
|
313
|
+
return prev;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return [
|
|
317
|
+
...prev,
|
|
318
|
+
{
|
|
319
|
+
sessionId: event.sessionId,
|
|
320
|
+
serverId: event.serverId,
|
|
321
|
+
serverName: event.serverName,
|
|
322
|
+
serverUrl: event.serverUrl,
|
|
323
|
+
state: event.state,
|
|
324
|
+
createdAt: event.createdAt ? new Date(event.createdAt) : undefined,
|
|
325
|
+
tools: [],
|
|
326
|
+
},
|
|
327
|
+
];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
case 'tools_discovered': {
|
|
332
|
+
// Preload UI resources for instant loading when tools are discovered
|
|
333
|
+
if (clientRef.current && event.tools?.length) {
|
|
334
|
+
clientRef.current.preloadToolUiResources(event.sessionId, event.tools);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return prev.map((c: McpConnection) =>
|
|
338
|
+
c.sessionId === event.sessionId ? { ...c, tools: event.tools, state: 'READY' } : c
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
case 'auth_required': {
|
|
343
|
+
// Handle OAuth redirect
|
|
344
|
+
if (event.authUrl) {
|
|
345
|
+
onLog?.('info', `OAuth required - redirecting to ${event.authUrl}`, { authUrl: event.authUrl });
|
|
346
|
+
|
|
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
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return prev.map((c: McpConnection) =>
|
|
357
|
+
c.sessionId === event.sessionId ? { ...c, state: 'AUTHENTICATING', authUrl: event.authUrl } : c
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
case 'error': {
|
|
362
|
+
return prev.map((c: McpConnection) =>
|
|
363
|
+
c.sessionId === event.sessionId ? { ...c, state: 'FAILED', error: event.error } : c
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
case 'disconnected': {
|
|
368
|
+
return prev.filter((c: McpConnection) => c.sessionId !== event.sessionId);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
default:
|
|
372
|
+
return prev;
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}, [onLog, onRedirect]);
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Load sessions from server
|
|
379
|
+
*/
|
|
380
|
+
const loadSessions = useCallback(async () => {
|
|
381
|
+
if (!clientRef.current) return;
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
setIsInitializing(true);
|
|
385
|
+
|
|
386
|
+
const result = await clientRef.current.getSessions();
|
|
387
|
+
const sessions = result.sessions || [];
|
|
388
|
+
|
|
389
|
+
// Initialize connections
|
|
390
|
+
if (isMountedRef.current) {
|
|
391
|
+
setConnections(
|
|
392
|
+
sessions.map((s: SessionInfo) => ({
|
|
393
|
+
sessionId: s.sessionId,
|
|
394
|
+
serverId: s.serverId ?? 'unknown',
|
|
395
|
+
serverName: s.serverName ?? 'Unknown Server',
|
|
396
|
+
serverUrl: s.serverUrl,
|
|
397
|
+
transport: s.transport,
|
|
398
|
+
state: (s.active === false ? 'AUTHENTICATING' : 'VALIDATING') as McpConnectionState,
|
|
399
|
+
createdAt: new Date(s.createdAt),
|
|
400
|
+
tools: [],
|
|
401
|
+
}))
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Validate each session in parallel
|
|
406
|
+
await Promise.all(
|
|
407
|
+
sessions.map(async (session: SessionInfo) => {
|
|
408
|
+
if (clientRef.current) {
|
|
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);
|
|
415
|
+
await clientRef.current.restoreSession(session.sessionId);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.error(`[useMcp] Failed to validate session ${session.sessionId}:`, error);
|
|
418
|
+
} finally {
|
|
419
|
+
suppressAuthRedirectSessionsRef.current.delete(session.sessionId);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
})
|
|
423
|
+
);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
console.error('[useMcp] Failed to load sessions:', error);
|
|
426
|
+
onLog?.('error', 'Failed to load sessions', { error });
|
|
427
|
+
} finally {
|
|
428
|
+
if (isMountedRef.current) {
|
|
429
|
+
setIsInitializing(false);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}, [onLog]);
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Connect to an MCP server
|
|
436
|
+
*/
|
|
437
|
+
const connect = useCallback(
|
|
438
|
+
async (params: {
|
|
439
|
+
serverId: string;
|
|
440
|
+
serverName: string;
|
|
441
|
+
serverUrl: string;
|
|
442
|
+
callbackUrl: string;
|
|
443
|
+
transportType?: 'sse' | 'streamable_http';
|
|
444
|
+
}): Promise<string> => {
|
|
445
|
+
if (!clientRef.current) {
|
|
446
|
+
throw new Error('SSE client not initialized');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const result = await clientRef.current.connectToServer(params);
|
|
450
|
+
return result.sessionId;
|
|
451
|
+
},
|
|
452
|
+
[]
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Disconnect from an MCP server
|
|
457
|
+
*/
|
|
458
|
+
const disconnect = useCallback(async (sessionId: string): Promise<void> => {
|
|
459
|
+
if (!clientRef.current) {
|
|
460
|
+
throw new Error('SSE client not initialized');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
await clientRef.current.disconnectFromServer(sessionId);
|
|
464
|
+
|
|
465
|
+
// Remove from local state
|
|
466
|
+
if (isMountedRef.current) {
|
|
467
|
+
setConnections((prev: McpConnection[]) => prev.filter((c: McpConnection) => c.sessionId !== sessionId));
|
|
468
|
+
}
|
|
469
|
+
}, []);
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Refresh all connections
|
|
473
|
+
*/
|
|
474
|
+
const refresh = useCallback(async () => {
|
|
475
|
+
await loadSessions();
|
|
476
|
+
}, [loadSessions]);
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Manually connect SSE
|
|
480
|
+
*/
|
|
481
|
+
const connectSSE = useCallback(() => {
|
|
482
|
+
clientRef.current?.connect();
|
|
483
|
+
}, []);
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Manually disconnect SSE
|
|
487
|
+
*/
|
|
488
|
+
const disconnectSSE = useCallback(() => {
|
|
489
|
+
clientRef.current?.disconnect();
|
|
490
|
+
}, []);
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Complete OAuth authorization
|
|
494
|
+
*/
|
|
495
|
+
const finishAuth = useCallback(async (sessionId: string, code: string): Promise<FinishAuthResult> => {
|
|
496
|
+
if (!clientRef.current) {
|
|
497
|
+
throw new Error('SSE client not initialized');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return await clientRef.current.finishAuth(sessionId, code);
|
|
501
|
+
}, []);
|
|
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
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Call a tool
|
|
517
|
+
*/
|
|
518
|
+
const callTool = useCallback(
|
|
519
|
+
async (
|
|
520
|
+
sessionId: string,
|
|
521
|
+
toolName: string,
|
|
522
|
+
toolArgs: Record<string, unknown>
|
|
523
|
+
): Promise<unknown> => {
|
|
524
|
+
if (!clientRef.current) {
|
|
525
|
+
throw new Error('SSE client not initialized');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return await clientRef.current.callTool(sessionId, toolName, toolArgs);
|
|
529
|
+
},
|
|
530
|
+
[]
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* List tools (refresh tool list)
|
|
535
|
+
*/
|
|
536
|
+
const listTools = useCallback(async (sessionId: string): Promise<ListToolsRpcResult> => {
|
|
537
|
+
if (!clientRef.current) {
|
|
538
|
+
throw new Error('SSE client not initialized');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return await clientRef.current.listTools(sessionId);
|
|
542
|
+
}, []);
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* List prompts
|
|
546
|
+
*/
|
|
547
|
+
const listPrompts = useCallback(async (sessionId: string): Promise<ListPromptsResult> => {
|
|
548
|
+
if (!clientRef.current) {
|
|
549
|
+
throw new Error('SSE client not initialized');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return await clientRef.current.listPrompts(sessionId);
|
|
553
|
+
}, []);
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Get a specific prompt
|
|
557
|
+
*/
|
|
558
|
+
const getPrompt = useCallback(
|
|
559
|
+
async (sessionId: string, name: string, args?: Record<string, string>): Promise<unknown> => {
|
|
560
|
+
if (!clientRef.current) {
|
|
561
|
+
throw new Error('SSE client not initialized');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return await clientRef.current.getPrompt(sessionId, name, args);
|
|
565
|
+
},
|
|
566
|
+
[]
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* List resources
|
|
571
|
+
*/
|
|
572
|
+
const listResources = useCallback(async (sessionId: string): Promise<ListResourcesResult> => {
|
|
573
|
+
if (!clientRef.current) {
|
|
574
|
+
throw new Error('SSE client not initialized');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return await clientRef.current.listResources(sessionId);
|
|
578
|
+
}, []);
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Read a specific resource
|
|
582
|
+
*/
|
|
583
|
+
const readResource = useCallback(async (sessionId: string, uri: string): Promise<unknown> => {
|
|
584
|
+
if (!clientRef.current) {
|
|
585
|
+
throw new Error('SSE client not initialized');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return await clientRef.current.readResource(sessionId, uri);
|
|
589
|
+
}, []);
|
|
590
|
+
|
|
591
|
+
// Utility functions
|
|
592
|
+
const getConnection = useCallback(
|
|
593
|
+
(sessionId: string) => connections.find((c: McpConnection) => c.sessionId === sessionId),
|
|
594
|
+
[connections]
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
const getConnectionByServerId = useCallback(
|
|
598
|
+
(serverId: string) => connections.find((c: McpConnection) => c.serverId === serverId),
|
|
599
|
+
[connections]
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
const isServerConnected = useCallback(
|
|
603
|
+
(serverId: string) => {
|
|
604
|
+
const conn = getConnectionByServerId(serverId);
|
|
605
|
+
return conn ? conn.state === 'CONNECTED' || conn.state === 'DISCOVERING' || conn.state === 'READY' : false;
|
|
606
|
+
},
|
|
607
|
+
[getConnectionByServerId]
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
const getTools = useCallback(
|
|
611
|
+
(sessionId: string) => {
|
|
612
|
+
const conn = getConnection(sessionId);
|
|
613
|
+
return conn?.tools || [];
|
|
614
|
+
},
|
|
615
|
+
[getConnection]
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
return {
|
|
619
|
+
connections,
|
|
620
|
+
status,
|
|
621
|
+
isInitializing,
|
|
622
|
+
connect,
|
|
623
|
+
disconnect,
|
|
624
|
+
getConnection,
|
|
625
|
+
getConnectionByServerId,
|
|
626
|
+
isServerConnected,
|
|
627
|
+
getTools,
|
|
628
|
+
refresh,
|
|
629
|
+
connectSSE,
|
|
630
|
+
disconnectSSE,
|
|
631
|
+
finishAuth,
|
|
632
|
+
resumeAuth,
|
|
633
|
+
callTool,
|
|
634
|
+
listTools,
|
|
635
|
+
listPrompts,
|
|
636
|
+
getPrompt,
|
|
637
|
+
listResources,
|
|
638
|
+
readResource,
|
|
639
|
+
sseClient: clientRef.current,
|
|
640
|
+
};
|
|
641
|
+
}
|