@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,617 +1,617 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useMcp Vue Composable
|
|
3
|
-
* Manages MCP connections with SSE-based real-time updates
|
|
4
|
-
* Based on Cloudflare's agents pattern
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { ref, onMounted, onUnmounted, watch, computed, shallowRef } from 'vue';
|
|
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
|
-
export interface McpConnection {
|
|
76
|
-
sessionId: string;
|
|
77
|
-
serverId: string;
|
|
78
|
-
serverName: string;
|
|
79
|
-
serverUrl?: string;
|
|
80
|
-
transport?: string;
|
|
81
|
-
state: McpConnectionState;
|
|
82
|
-
tools: ToolInfo[];
|
|
83
|
-
authUrl?: string;
|
|
84
|
-
error?: string;
|
|
85
|
-
createdAt?: Date;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface McpClient {
|
|
89
|
-
/**
|
|
90
|
-
* All connections (Represents a Reactive Ref)
|
|
91
|
-
*/
|
|
92
|
-
connections: { value: McpConnection[] };
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* SSE connection status (Represents a Reactive Ref)
|
|
96
|
-
*/
|
|
97
|
-
status: { value: 'connecting' | 'connected' | 'disconnected' | 'error' };
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Whether initializing (Represents a Reactive Ref)
|
|
101
|
-
*/
|
|
102
|
-
isInitializing: { value: boolean };
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Connect to an MCP server
|
|
106
|
-
*/
|
|
107
|
-
connect: (params: {
|
|
108
|
-
serverId: string;
|
|
109
|
-
serverName: string;
|
|
110
|
-
serverUrl: string;
|
|
111
|
-
callbackUrl: string;
|
|
112
|
-
transportType?: 'sse' | 'streamable_http';
|
|
113
|
-
}) => Promise<string>;
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Disconnect from an MCP server
|
|
117
|
-
*/
|
|
118
|
-
disconnect: (sessionId: string) => Promise<void>;
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Get connection by session ID
|
|
122
|
-
*/
|
|
123
|
-
getConnection: (sessionId: string) => McpConnection | undefined;
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Get connection by server ID
|
|
127
|
-
*/
|
|
128
|
-
getConnectionByServerId: (serverId: string) => McpConnection | undefined;
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Check if server is connected
|
|
132
|
-
*/
|
|
133
|
-
isServerConnected: (serverId: string) => boolean;
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Get tools for a session
|
|
137
|
-
*/
|
|
138
|
-
getTools: (sessionId: string) => ToolInfo[];
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Refresh all connections
|
|
142
|
-
*/
|
|
143
|
-
refresh: () => Promise<void>;
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Manually connect SSE
|
|
147
|
-
*/
|
|
148
|
-
connectSSE: () => void;
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Manually disconnect SSE
|
|
152
|
-
*/
|
|
153
|
-
disconnectSSE: () => void;
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Complete OAuth authorization
|
|
157
|
-
*/
|
|
158
|
-
finishAuth: (sessionId: string, code: string) => Promise<FinishAuthResult>;
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Explicitly resume OAuth flow for an existing session
|
|
162
|
-
*/
|
|
163
|
-
resumeAuth: (sessionId: string) => Promise<void>;
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Call a tool from a session
|
|
167
|
-
*/
|
|
168
|
-
callTool: (
|
|
169
|
-
sessionId: string,
|
|
170
|
-
toolName: string,
|
|
171
|
-
toolArgs: Record<string, unknown>
|
|
172
|
-
) => Promise<unknown>;
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* List available tools for a session
|
|
176
|
-
*/
|
|
177
|
-
listTools: (sessionId: string) => Promise<ListToolsRpcResult>;
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* List available prompts for a session
|
|
181
|
-
*/
|
|
182
|
-
listPrompts: (sessionId: string) => Promise<ListPromptsResult>;
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Get a specific prompt with arguments
|
|
186
|
-
*/
|
|
187
|
-
getPrompt: (sessionId: string, name: string, args?: Record<string, string>) => Promise<unknown>;
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* List available resources for a session
|
|
191
|
-
*/
|
|
192
|
-
listResources: (sessionId: string) => Promise<ListResourcesResult>;
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Read a specific resource
|
|
196
|
-
*/
|
|
197
|
-
readResource: (sessionId: string, uri: string) => Promise<unknown>;
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Access the underlying SSEClient instance (for advanced usage like AppHost)
|
|
201
|
-
*/
|
|
202
|
-
sseClient: SSEClient | null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Vue Composable for MCP connection management with SSE
|
|
207
|
-
*/
|
|
208
|
-
export function useMcp(options: UseMcpOptions): McpClient {
|
|
209
|
-
const {
|
|
210
|
-
url,
|
|
211
|
-
identity,
|
|
212
|
-
authToken,
|
|
213
|
-
autoConnect = true,
|
|
214
|
-
autoInitialize = true,
|
|
215
|
-
onConnectionEvent,
|
|
216
|
-
onLog,
|
|
217
|
-
onRedirect,
|
|
218
|
-
} = options;
|
|
219
|
-
|
|
220
|
-
// Use shallowRef for client instance as it doesn't need deep reactivity
|
|
221
|
-
const clientRef = shallowRef<SSEClient | null>(null);
|
|
222
|
-
const isMountedRef = ref(true);
|
|
223
|
-
const suppressAuthRedirectSessions = ref(new Set<string>());
|
|
224
|
-
|
|
225
|
-
const connections = ref<McpConnection[]>([]);
|
|
226
|
-
const status = ref<'connecting' | 'connected' | 'disconnected' | 'error'>('disconnected');
|
|
227
|
-
const isInitializing = ref(false);
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Update connections based on event
|
|
231
|
-
*/
|
|
232
|
-
const updateConnectionsFromEvent = (event: McpConnectionEvent) => {
|
|
233
|
-
if (!isMountedRef.value) return;
|
|
234
|
-
|
|
235
|
-
const isTransientReconnectState = (state: McpConnectionState): boolean =>
|
|
236
|
-
state === 'INITIALIZING' ||
|
|
237
|
-
state === 'VALIDATING' ||
|
|
238
|
-
state === 'RECONNECTING' ||
|
|
239
|
-
state === 'CONNECTING' ||
|
|
240
|
-
state === 'CONNECTED' ||
|
|
241
|
-
state === 'DISCOVERING';
|
|
242
|
-
|
|
243
|
-
switch (event.type) {
|
|
244
|
-
case 'state_changed': {
|
|
245
|
-
const existing = connections.value.find((c) => c.sessionId === event.sessionId);
|
|
246
|
-
if (existing) {
|
|
247
|
-
// In stateless per-request transport, tool calls can emit transient reconnect states.
|
|
248
|
-
// Keep READY sticky to avoid UI flicker from READY -> CONNECTING -> CONNECTED.
|
|
249
|
-
const nextState =
|
|
250
|
-
existing.state === 'READY' && isTransientReconnectState(event.state)
|
|
251
|
-
? existing.state
|
|
252
|
-
: event.state;
|
|
253
|
-
|
|
254
|
-
const index = connections.value.indexOf(existing);
|
|
255
|
-
connections.value[index] = {
|
|
256
|
-
...existing,
|
|
257
|
-
state: nextState,
|
|
258
|
-
// update createdAt if present in event, otherwise keep existing
|
|
259
|
-
createdAt: event.createdAt ? new Date(event.createdAt) : existing.createdAt
|
|
260
|
-
};
|
|
261
|
-
} else {
|
|
262
|
-
// Fix: Don't add back disconnected sessions that were just removed
|
|
263
|
-
if (event.state === 'DISCONNECTED') {
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
connections.value = [...connections.value, {
|
|
268
|
-
sessionId: event.sessionId,
|
|
269
|
-
serverId: event.serverId,
|
|
270
|
-
serverName: event.serverName,
|
|
271
|
-
state: event.state,
|
|
272
|
-
createdAt: event.createdAt ? new Date(event.createdAt) : undefined,
|
|
273
|
-
tools: [],
|
|
274
|
-
}];
|
|
275
|
-
}
|
|
276
|
-
break;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
case 'tools_discovered': {
|
|
280
|
-
const index = connections.value.findIndex((c) => c.sessionId === event.sessionId);
|
|
281
|
-
if (index !== -1) {
|
|
282
|
-
connections.value[index] = { ...connections.value[index], tools: event.tools, state: 'READY' };
|
|
283
|
-
}
|
|
284
|
-
break;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
case 'auth_required': {
|
|
288
|
-
// Handle OAuth redirect
|
|
289
|
-
if (event.authUrl) {
|
|
290
|
-
onLog?.('info', `OAuth required - redirecting to ${event.authUrl}`, { authUrl: event.authUrl });
|
|
291
|
-
|
|
292
|
-
// Suppress redirects/popups for background auto-restore on page load.
|
|
293
|
-
if (!suppressAuthRedirectSessions.value.has(event.sessionId)) {
|
|
294
|
-
if (onRedirect) {
|
|
295
|
-
onRedirect(event.authUrl);
|
|
296
|
-
} else if (typeof window !== 'undefined') {
|
|
297
|
-
window.location.href = event.authUrl;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
const index = connections.value.findIndex((c) => c.sessionId === event.sessionId);
|
|
302
|
-
if (index !== -1) {
|
|
303
|
-
connections.value[index] = { ...connections.value[index], state: 'AUTHENTICATING', authUrl: event.authUrl };
|
|
304
|
-
}
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
case 'error': {
|
|
309
|
-
const index = connections.value.findIndex((c) => c.sessionId === event.sessionId);
|
|
310
|
-
if (index !== -1) {
|
|
311
|
-
connections.value[index] = { ...connections.value[index], state: 'FAILED', error: event.error };
|
|
312
|
-
}
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
case 'disconnected': {
|
|
317
|
-
connections.value = connections.value.filter((c) => c.sessionId !== event.sessionId);
|
|
318
|
-
break;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Load sessions from server
|
|
325
|
-
*/
|
|
326
|
-
const loadSessions = async () => {
|
|
327
|
-
if (!clientRef.value) return;
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
isInitializing.value = true;
|
|
331
|
-
|
|
332
|
-
const result = await clientRef.value.getSessions();
|
|
333
|
-
const sessions = result.sessions || [];
|
|
334
|
-
|
|
335
|
-
// Initialize connections
|
|
336
|
-
if (isMountedRef.value) {
|
|
337
|
-
connections.value = sessions.map((s: SessionInfo) => ({
|
|
338
|
-
sessionId: s.sessionId,
|
|
339
|
-
serverId: s.serverId ?? 'unknown',
|
|
340
|
-
serverName: s.serverName ?? 'Unknown Server',
|
|
341
|
-
serverUrl: s.serverUrl,
|
|
342
|
-
transport: s.transport,
|
|
343
|
-
state: (s.active === false ? 'AUTHENTICATING' : 'VALIDATING') as McpConnectionState,
|
|
344
|
-
createdAt: new Date(s.createdAt),
|
|
345
|
-
tools: [],
|
|
346
|
-
}));
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Validate each session in parallel
|
|
350
|
-
await Promise.all(
|
|
351
|
-
sessions.map(async (session: SessionInfo) => {
|
|
352
|
-
if (clientRef.value) {
|
|
353
|
-
try {
|
|
354
|
-
// Pending auth sessions should not auto-trigger popup/redirect on reload.
|
|
355
|
-
if (session.active === false) {
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
suppressAuthRedirectSessions.value.add(session.sessionId);
|
|
359
|
-
await clientRef.value.restoreSession(session.sessionId);
|
|
360
|
-
} catch (error) {
|
|
361
|
-
console.error(`[useMcp] Failed to validate session ${session.sessionId}:`, error);
|
|
362
|
-
} finally {
|
|
363
|
-
suppressAuthRedirectSessions.value.delete(session.sessionId);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
})
|
|
367
|
-
);
|
|
368
|
-
} catch (error) {
|
|
369
|
-
console.error('[useMcp] Failed to load sessions:', error);
|
|
370
|
-
onLog?.('error', 'Failed to load sessions', { error });
|
|
371
|
-
} finally {
|
|
372
|
-
if (isMountedRef.value) {
|
|
373
|
-
isInitializing.value = false;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Initialize SSE client
|
|
380
|
-
*/
|
|
381
|
-
const initClient = () => {
|
|
382
|
-
// Disconnect existing if any
|
|
383
|
-
if (clientRef.value) {
|
|
384
|
-
clientRef.value.disconnect();
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const clientOptions: SSEClientOptions = {
|
|
388
|
-
url,
|
|
389
|
-
identity,
|
|
390
|
-
authToken,
|
|
391
|
-
onConnectionEvent: (event) => {
|
|
392
|
-
// Update local state based on event
|
|
393
|
-
updateConnectionsFromEvent(event);
|
|
394
|
-
|
|
395
|
-
// Call user callback
|
|
396
|
-
onConnectionEvent?.(event);
|
|
397
|
-
},
|
|
398
|
-
onObservabilityEvent: (event) => {
|
|
399
|
-
onLog?.(event.level || 'info', event.message || event.displayMessage || 'No message', event.metadata);
|
|
400
|
-
},
|
|
401
|
-
onStatusChange: (newStatus) => {
|
|
402
|
-
if (isMountedRef.value) {
|
|
403
|
-
status.value = newStatus;
|
|
404
|
-
}
|
|
405
|
-
},
|
|
406
|
-
debug: options.debug,
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
const client = new SSEClient(clientOptions);
|
|
410
|
-
clientRef.value = client;
|
|
411
|
-
|
|
412
|
-
if (autoConnect) {
|
|
413
|
-
client.connect();
|
|
414
|
-
|
|
415
|
-
if (autoInitialize) {
|
|
416
|
-
loadSessions();
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
onMounted(() => {
|
|
422
|
-
isMountedRef.value = true;
|
|
423
|
-
initClient();
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
onUnmounted(() => {
|
|
427
|
-
isMountedRef.value = false;
|
|
428
|
-
clientRef.value?.disconnect();
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Connect to an MCP server
|
|
433
|
-
*/
|
|
434
|
-
const connect = async (params: {
|
|
435
|
-
serverId: string;
|
|
436
|
-
serverName: string;
|
|
437
|
-
serverUrl: string;
|
|
438
|
-
callbackUrl: string;
|
|
439
|
-
transportType?: 'sse' | 'streamable_http';
|
|
440
|
-
}): Promise<string> => {
|
|
441
|
-
if (!clientRef.value) {
|
|
442
|
-
throw new Error('SSE client not initialized');
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const result = await clientRef.value.connectToServer(params);
|
|
446
|
-
return result.sessionId;
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Disconnect from an MCP server
|
|
451
|
-
*/
|
|
452
|
-
const disconnect = async (sessionId: string): Promise<void> => {
|
|
453
|
-
if (!clientRef.value) {
|
|
454
|
-
throw new Error('SSE client not initialized');
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
await clientRef.value.disconnectFromServer(sessionId);
|
|
458
|
-
|
|
459
|
-
// Remove from local state
|
|
460
|
-
if (isMountedRef.value) {
|
|
461
|
-
connections.value = connections.value.filter((c) => c.sessionId !== sessionId);
|
|
462
|
-
}
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Refresh all connections
|
|
467
|
-
*/
|
|
468
|
-
const refresh = async () => {
|
|
469
|
-
await loadSessions();
|
|
470
|
-
};
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Manually connect SSE
|
|
474
|
-
*/
|
|
475
|
-
const connectSSE = () => {
|
|
476
|
-
clientRef.value?.connect();
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Manually disconnect SSE
|
|
481
|
-
*/
|
|
482
|
-
const disconnectSSE = () => {
|
|
483
|
-
clientRef.value?.disconnect();
|
|
484
|
-
};
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Complete OAuth authorization
|
|
488
|
-
*/
|
|
489
|
-
const finishAuth = async (sessionId: string, code: string): Promise<FinishAuthResult> => {
|
|
490
|
-
if (!clientRef.value) {
|
|
491
|
-
throw new Error('SSE client not initialized');
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return await clientRef.value.finishAuth(sessionId, code);
|
|
495
|
-
};
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Explicit user action to resume OAuth for an existing pending session.
|
|
499
|
-
*/
|
|
500
|
-
const resumeAuth = async (sessionId: string): Promise<void> => {
|
|
501
|
-
if (!clientRef.value) {
|
|
502
|
-
throw new Error('SSE client not initialized');
|
|
503
|
-
}
|
|
504
|
-
suppressAuthRedirectSessions.value.delete(sessionId);
|
|
505
|
-
await clientRef.value.restoreSession(sessionId);
|
|
506
|
-
};
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Call a tool
|
|
510
|
-
*/
|
|
511
|
-
const callTool = async (
|
|
512
|
-
sessionId: string,
|
|
513
|
-
toolName: string,
|
|
514
|
-
toolArgs: Record<string, unknown>
|
|
515
|
-
): Promise<unknown> => {
|
|
516
|
-
if (!clientRef.value) {
|
|
517
|
-
throw new Error('SSE client not initialized');
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
return await clientRef.value.callTool(sessionId, toolName, toolArgs);
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
/**
|
|
524
|
-
* List tools (refresh tool list)
|
|
525
|
-
*/
|
|
526
|
-
const listTools = async (sessionId: string): Promise<ListToolsRpcResult> => {
|
|
527
|
-
if (!clientRef.value) {
|
|
528
|
-
throw new Error('SSE client not initialized');
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
return await clientRef.value.listTools(sessionId);
|
|
532
|
-
};
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* List prompts
|
|
536
|
-
*/
|
|
537
|
-
const listPrompts = async (sessionId: string): Promise<ListPromptsResult> => {
|
|
538
|
-
if (!clientRef.value) {
|
|
539
|
-
throw new Error('SSE client not initialized');
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
return await clientRef.value.listPrompts(sessionId);
|
|
543
|
-
};
|
|
544
|
-
|
|
545
|
-
/**
|
|
546
|
-
* Get a specific prompt
|
|
547
|
-
*/
|
|
548
|
-
const getPrompt = async (sessionId: string, name: string, args?: Record<string, string>): Promise<unknown> => {
|
|
549
|
-
if (!clientRef.value) {
|
|
550
|
-
throw new Error('SSE client not initialized');
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return await clientRef.value.getPrompt(sessionId, name, args);
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* List resources
|
|
558
|
-
*/
|
|
559
|
-
const listResources = async (sessionId: string): Promise<ListResourcesResult> => {
|
|
560
|
-
if (!clientRef.value) {
|
|
561
|
-
throw new Error('SSE client not initialized');
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
return await clientRef.value.listResources(sessionId);
|
|
565
|
-
};
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* Read a specific resource
|
|
569
|
-
*/
|
|
570
|
-
const readResource = async (sessionId: string, uri: string): Promise<unknown> => {
|
|
571
|
-
if (!clientRef.value) {
|
|
572
|
-
throw new Error('SSE client not initialized');
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
return await clientRef.value.readResource(sessionId, uri);
|
|
576
|
-
};
|
|
577
|
-
|
|
578
|
-
// Utility functions
|
|
579
|
-
const getConnection = (sessionId: string) => connections.value.find((c) => c.sessionId === sessionId);
|
|
580
|
-
|
|
581
|
-
const getConnectionByServerId = (serverId: string) => connections.value.find((c) => c.serverId === serverId);
|
|
582
|
-
|
|
583
|
-
const isServerConnected = (serverId: string) => {
|
|
584
|
-
const conn = getConnectionByServerId(serverId);
|
|
585
|
-
return conn ? conn.state === 'CONNECTED' || conn.state === 'DISCOVERING' || conn.state === 'READY' : false;
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
const getTools = (sessionId: string) => {
|
|
589
|
-
const conn = getConnection(sessionId);
|
|
590
|
-
return conn?.tools || [];
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
return {
|
|
594
|
-
// Return them as Ref objects so they can be destructured and stay reactive
|
|
595
|
-
connections: connections as unknown as { value: McpConnection[] },
|
|
596
|
-
status: status as unknown as { value: 'connecting' | 'connected' | 'disconnected' | 'error' },
|
|
597
|
-
isInitializing: isInitializing as unknown as { value: boolean },
|
|
598
|
-
connect,
|
|
599
|
-
disconnect,
|
|
600
|
-
getConnection,
|
|
601
|
-
getConnectionByServerId,
|
|
602
|
-
isServerConnected,
|
|
603
|
-
getTools,
|
|
604
|
-
refresh,
|
|
605
|
-
connectSSE,
|
|
606
|
-
disconnectSSE,
|
|
607
|
-
finishAuth,
|
|
608
|
-
resumeAuth,
|
|
609
|
-
callTool,
|
|
610
|
-
listTools,
|
|
611
|
-
listPrompts,
|
|
612
|
-
getPrompt,
|
|
613
|
-
listResources,
|
|
614
|
-
readResource,
|
|
615
|
-
sseClient: clientRef.value,
|
|
616
|
-
};
|
|
617
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* useMcp Vue Composable
|
|
3
|
+
* Manages MCP connections with SSE-based real-time updates
|
|
4
|
+
* Based on Cloudflare's agents pattern
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ref, onMounted, onUnmounted, watch, computed, shallowRef } from 'vue';
|
|
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
|
+
export interface McpConnection {
|
|
76
|
+
sessionId: string;
|
|
77
|
+
serverId: string;
|
|
78
|
+
serverName: string;
|
|
79
|
+
serverUrl?: string;
|
|
80
|
+
transport?: string;
|
|
81
|
+
state: McpConnectionState;
|
|
82
|
+
tools: ToolInfo[];
|
|
83
|
+
authUrl?: string;
|
|
84
|
+
error?: string;
|
|
85
|
+
createdAt?: Date;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface McpClient {
|
|
89
|
+
/**
|
|
90
|
+
* All connections (Represents a Reactive Ref)
|
|
91
|
+
*/
|
|
92
|
+
connections: { value: McpConnection[] };
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* SSE connection status (Represents a Reactive Ref)
|
|
96
|
+
*/
|
|
97
|
+
status: { value: 'connecting' | 'connected' | 'disconnected' | 'error' };
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Whether initializing (Represents a Reactive Ref)
|
|
101
|
+
*/
|
|
102
|
+
isInitializing: { value: boolean };
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Connect to an MCP server
|
|
106
|
+
*/
|
|
107
|
+
connect: (params: {
|
|
108
|
+
serverId: string;
|
|
109
|
+
serverName: string;
|
|
110
|
+
serverUrl: string;
|
|
111
|
+
callbackUrl: string;
|
|
112
|
+
transportType?: 'sse' | 'streamable_http';
|
|
113
|
+
}) => Promise<string>;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Disconnect from an MCP server
|
|
117
|
+
*/
|
|
118
|
+
disconnect: (sessionId: string) => Promise<void>;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get connection by session ID
|
|
122
|
+
*/
|
|
123
|
+
getConnection: (sessionId: string) => McpConnection | undefined;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get connection by server ID
|
|
127
|
+
*/
|
|
128
|
+
getConnectionByServerId: (serverId: string) => McpConnection | undefined;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if server is connected
|
|
132
|
+
*/
|
|
133
|
+
isServerConnected: (serverId: string) => boolean;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get tools for a session
|
|
137
|
+
*/
|
|
138
|
+
getTools: (sessionId: string) => ToolInfo[];
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Refresh all connections
|
|
142
|
+
*/
|
|
143
|
+
refresh: () => Promise<void>;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Manually connect SSE
|
|
147
|
+
*/
|
|
148
|
+
connectSSE: () => void;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Manually disconnect SSE
|
|
152
|
+
*/
|
|
153
|
+
disconnectSSE: () => void;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Complete OAuth authorization
|
|
157
|
+
*/
|
|
158
|
+
finishAuth: (sessionId: string, code: string) => Promise<FinishAuthResult>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Explicitly resume OAuth flow for an existing session
|
|
162
|
+
*/
|
|
163
|
+
resumeAuth: (sessionId: string) => Promise<void>;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Call a tool from a session
|
|
167
|
+
*/
|
|
168
|
+
callTool: (
|
|
169
|
+
sessionId: string,
|
|
170
|
+
toolName: string,
|
|
171
|
+
toolArgs: Record<string, unknown>
|
|
172
|
+
) => Promise<unknown>;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* List available tools for a session
|
|
176
|
+
*/
|
|
177
|
+
listTools: (sessionId: string) => Promise<ListToolsRpcResult>;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* List available prompts for a session
|
|
181
|
+
*/
|
|
182
|
+
listPrompts: (sessionId: string) => Promise<ListPromptsResult>;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get a specific prompt with arguments
|
|
186
|
+
*/
|
|
187
|
+
getPrompt: (sessionId: string, name: string, args?: Record<string, string>) => Promise<unknown>;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* List available resources for a session
|
|
191
|
+
*/
|
|
192
|
+
listResources: (sessionId: string) => Promise<ListResourcesResult>;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Read a specific resource
|
|
196
|
+
*/
|
|
197
|
+
readResource: (sessionId: string, uri: string) => Promise<unknown>;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Access the underlying SSEClient instance (for advanced usage like AppHost)
|
|
201
|
+
*/
|
|
202
|
+
sseClient: SSEClient | null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Vue Composable for MCP connection management with SSE
|
|
207
|
+
*/
|
|
208
|
+
export function useMcp(options: UseMcpOptions): McpClient {
|
|
209
|
+
const {
|
|
210
|
+
url,
|
|
211
|
+
identity,
|
|
212
|
+
authToken,
|
|
213
|
+
autoConnect = true,
|
|
214
|
+
autoInitialize = true,
|
|
215
|
+
onConnectionEvent,
|
|
216
|
+
onLog,
|
|
217
|
+
onRedirect,
|
|
218
|
+
} = options;
|
|
219
|
+
|
|
220
|
+
// Use shallowRef for client instance as it doesn't need deep reactivity
|
|
221
|
+
const clientRef = shallowRef<SSEClient | null>(null);
|
|
222
|
+
const isMountedRef = ref(true);
|
|
223
|
+
const suppressAuthRedirectSessions = ref(new Set<string>());
|
|
224
|
+
|
|
225
|
+
const connections = ref<McpConnection[]>([]);
|
|
226
|
+
const status = ref<'connecting' | 'connected' | 'disconnected' | 'error'>('disconnected');
|
|
227
|
+
const isInitializing = ref(false);
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Update connections based on event
|
|
231
|
+
*/
|
|
232
|
+
const updateConnectionsFromEvent = (event: McpConnectionEvent) => {
|
|
233
|
+
if (!isMountedRef.value) return;
|
|
234
|
+
|
|
235
|
+
const isTransientReconnectState = (state: McpConnectionState): boolean =>
|
|
236
|
+
state === 'INITIALIZING' ||
|
|
237
|
+
state === 'VALIDATING' ||
|
|
238
|
+
state === 'RECONNECTING' ||
|
|
239
|
+
state === 'CONNECTING' ||
|
|
240
|
+
state === 'CONNECTED' ||
|
|
241
|
+
state === 'DISCOVERING';
|
|
242
|
+
|
|
243
|
+
switch (event.type) {
|
|
244
|
+
case 'state_changed': {
|
|
245
|
+
const existing = connections.value.find((c) => c.sessionId === event.sessionId);
|
|
246
|
+
if (existing) {
|
|
247
|
+
// In stateless per-request transport, tool calls can emit transient reconnect states.
|
|
248
|
+
// Keep READY sticky to avoid UI flicker from READY -> CONNECTING -> CONNECTED.
|
|
249
|
+
const nextState =
|
|
250
|
+
existing.state === 'READY' && isTransientReconnectState(event.state)
|
|
251
|
+
? existing.state
|
|
252
|
+
: event.state;
|
|
253
|
+
|
|
254
|
+
const index = connections.value.indexOf(existing);
|
|
255
|
+
connections.value[index] = {
|
|
256
|
+
...existing,
|
|
257
|
+
state: nextState,
|
|
258
|
+
// update createdAt if present in event, otherwise keep existing
|
|
259
|
+
createdAt: event.createdAt ? new Date(event.createdAt) : existing.createdAt
|
|
260
|
+
};
|
|
261
|
+
} else {
|
|
262
|
+
// Fix: Don't add back disconnected sessions that were just removed
|
|
263
|
+
if (event.state === 'DISCONNECTED') {
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
connections.value = [...connections.value, {
|
|
268
|
+
sessionId: event.sessionId,
|
|
269
|
+
serverId: event.serverId,
|
|
270
|
+
serverName: event.serverName,
|
|
271
|
+
state: event.state,
|
|
272
|
+
createdAt: event.createdAt ? new Date(event.createdAt) : undefined,
|
|
273
|
+
tools: [],
|
|
274
|
+
}];
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
case 'tools_discovered': {
|
|
280
|
+
const index = connections.value.findIndex((c) => c.sessionId === event.sessionId);
|
|
281
|
+
if (index !== -1) {
|
|
282
|
+
connections.value[index] = { ...connections.value[index], tools: event.tools, state: 'READY' };
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
case 'auth_required': {
|
|
288
|
+
// Handle OAuth redirect
|
|
289
|
+
if (event.authUrl) {
|
|
290
|
+
onLog?.('info', `OAuth required - redirecting to ${event.authUrl}`, { authUrl: event.authUrl });
|
|
291
|
+
|
|
292
|
+
// Suppress redirects/popups for background auto-restore on page load.
|
|
293
|
+
if (!suppressAuthRedirectSessions.value.has(event.sessionId)) {
|
|
294
|
+
if (onRedirect) {
|
|
295
|
+
onRedirect(event.authUrl);
|
|
296
|
+
} else if (typeof window !== 'undefined') {
|
|
297
|
+
window.location.href = event.authUrl;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const index = connections.value.findIndex((c) => c.sessionId === event.sessionId);
|
|
302
|
+
if (index !== -1) {
|
|
303
|
+
connections.value[index] = { ...connections.value[index], state: 'AUTHENTICATING', authUrl: event.authUrl };
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
case 'error': {
|
|
309
|
+
const index = connections.value.findIndex((c) => c.sessionId === event.sessionId);
|
|
310
|
+
if (index !== -1) {
|
|
311
|
+
connections.value[index] = { ...connections.value[index], state: 'FAILED', error: event.error };
|
|
312
|
+
}
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
case 'disconnected': {
|
|
317
|
+
connections.value = connections.value.filter((c) => c.sessionId !== event.sessionId);
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Load sessions from server
|
|
325
|
+
*/
|
|
326
|
+
const loadSessions = async () => {
|
|
327
|
+
if (!clientRef.value) return;
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
isInitializing.value = true;
|
|
331
|
+
|
|
332
|
+
const result = await clientRef.value.getSessions();
|
|
333
|
+
const sessions = result.sessions || [];
|
|
334
|
+
|
|
335
|
+
// Initialize connections
|
|
336
|
+
if (isMountedRef.value) {
|
|
337
|
+
connections.value = sessions.map((s: SessionInfo) => ({
|
|
338
|
+
sessionId: s.sessionId,
|
|
339
|
+
serverId: s.serverId ?? 'unknown',
|
|
340
|
+
serverName: s.serverName ?? 'Unknown Server',
|
|
341
|
+
serverUrl: s.serverUrl,
|
|
342
|
+
transport: s.transport,
|
|
343
|
+
state: (s.active === false ? 'AUTHENTICATING' : 'VALIDATING') as McpConnectionState,
|
|
344
|
+
createdAt: new Date(s.createdAt),
|
|
345
|
+
tools: [],
|
|
346
|
+
}));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Validate each session in parallel
|
|
350
|
+
await Promise.all(
|
|
351
|
+
sessions.map(async (session: SessionInfo) => {
|
|
352
|
+
if (clientRef.value) {
|
|
353
|
+
try {
|
|
354
|
+
// Pending auth sessions should not auto-trigger popup/redirect on reload.
|
|
355
|
+
if (session.active === false) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
suppressAuthRedirectSessions.value.add(session.sessionId);
|
|
359
|
+
await clientRef.value.restoreSession(session.sessionId);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.error(`[useMcp] Failed to validate session ${session.sessionId}:`, error);
|
|
362
|
+
} finally {
|
|
363
|
+
suppressAuthRedirectSessions.value.delete(session.sessionId);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
console.error('[useMcp] Failed to load sessions:', error);
|
|
370
|
+
onLog?.('error', 'Failed to load sessions', { error });
|
|
371
|
+
} finally {
|
|
372
|
+
if (isMountedRef.value) {
|
|
373
|
+
isInitializing.value = false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Initialize SSE client
|
|
380
|
+
*/
|
|
381
|
+
const initClient = () => {
|
|
382
|
+
// Disconnect existing if any
|
|
383
|
+
if (clientRef.value) {
|
|
384
|
+
clientRef.value.disconnect();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const clientOptions: SSEClientOptions = {
|
|
388
|
+
url,
|
|
389
|
+
identity,
|
|
390
|
+
authToken,
|
|
391
|
+
onConnectionEvent: (event) => {
|
|
392
|
+
// Update local state based on event
|
|
393
|
+
updateConnectionsFromEvent(event);
|
|
394
|
+
|
|
395
|
+
// Call user callback
|
|
396
|
+
onConnectionEvent?.(event);
|
|
397
|
+
},
|
|
398
|
+
onObservabilityEvent: (event) => {
|
|
399
|
+
onLog?.(event.level || 'info', event.message || event.displayMessage || 'No message', event.metadata);
|
|
400
|
+
},
|
|
401
|
+
onStatusChange: (newStatus) => {
|
|
402
|
+
if (isMountedRef.value) {
|
|
403
|
+
status.value = newStatus;
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
debug: options.debug,
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const client = new SSEClient(clientOptions);
|
|
410
|
+
clientRef.value = client;
|
|
411
|
+
|
|
412
|
+
if (autoConnect) {
|
|
413
|
+
client.connect();
|
|
414
|
+
|
|
415
|
+
if (autoInitialize) {
|
|
416
|
+
loadSessions();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
onMounted(() => {
|
|
422
|
+
isMountedRef.value = true;
|
|
423
|
+
initClient();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
onUnmounted(() => {
|
|
427
|
+
isMountedRef.value = false;
|
|
428
|
+
clientRef.value?.disconnect();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Connect to an MCP server
|
|
433
|
+
*/
|
|
434
|
+
const connect = async (params: {
|
|
435
|
+
serverId: string;
|
|
436
|
+
serverName: string;
|
|
437
|
+
serverUrl: string;
|
|
438
|
+
callbackUrl: string;
|
|
439
|
+
transportType?: 'sse' | 'streamable_http';
|
|
440
|
+
}): Promise<string> => {
|
|
441
|
+
if (!clientRef.value) {
|
|
442
|
+
throw new Error('SSE client not initialized');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const result = await clientRef.value.connectToServer(params);
|
|
446
|
+
return result.sessionId;
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Disconnect from an MCP server
|
|
451
|
+
*/
|
|
452
|
+
const disconnect = async (sessionId: string): Promise<void> => {
|
|
453
|
+
if (!clientRef.value) {
|
|
454
|
+
throw new Error('SSE client not initialized');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
await clientRef.value.disconnectFromServer(sessionId);
|
|
458
|
+
|
|
459
|
+
// Remove from local state
|
|
460
|
+
if (isMountedRef.value) {
|
|
461
|
+
connections.value = connections.value.filter((c) => c.sessionId !== sessionId);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Refresh all connections
|
|
467
|
+
*/
|
|
468
|
+
const refresh = async () => {
|
|
469
|
+
await loadSessions();
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Manually connect SSE
|
|
474
|
+
*/
|
|
475
|
+
const connectSSE = () => {
|
|
476
|
+
clientRef.value?.connect();
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Manually disconnect SSE
|
|
481
|
+
*/
|
|
482
|
+
const disconnectSSE = () => {
|
|
483
|
+
clientRef.value?.disconnect();
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Complete OAuth authorization
|
|
488
|
+
*/
|
|
489
|
+
const finishAuth = async (sessionId: string, code: string): Promise<FinishAuthResult> => {
|
|
490
|
+
if (!clientRef.value) {
|
|
491
|
+
throw new Error('SSE client not initialized');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return await clientRef.value.finishAuth(sessionId, code);
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Explicit user action to resume OAuth for an existing pending session.
|
|
499
|
+
*/
|
|
500
|
+
const resumeAuth = async (sessionId: string): Promise<void> => {
|
|
501
|
+
if (!clientRef.value) {
|
|
502
|
+
throw new Error('SSE client not initialized');
|
|
503
|
+
}
|
|
504
|
+
suppressAuthRedirectSessions.value.delete(sessionId);
|
|
505
|
+
await clientRef.value.restoreSession(sessionId);
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Call a tool
|
|
510
|
+
*/
|
|
511
|
+
const callTool = async (
|
|
512
|
+
sessionId: string,
|
|
513
|
+
toolName: string,
|
|
514
|
+
toolArgs: Record<string, unknown>
|
|
515
|
+
): Promise<unknown> => {
|
|
516
|
+
if (!clientRef.value) {
|
|
517
|
+
throw new Error('SSE client not initialized');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return await clientRef.value.callTool(sessionId, toolName, toolArgs);
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* List tools (refresh tool list)
|
|
525
|
+
*/
|
|
526
|
+
const listTools = async (sessionId: string): Promise<ListToolsRpcResult> => {
|
|
527
|
+
if (!clientRef.value) {
|
|
528
|
+
throw new Error('SSE client not initialized');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return await clientRef.value.listTools(sessionId);
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* List prompts
|
|
536
|
+
*/
|
|
537
|
+
const listPrompts = async (sessionId: string): Promise<ListPromptsResult> => {
|
|
538
|
+
if (!clientRef.value) {
|
|
539
|
+
throw new Error('SSE client not initialized');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return await clientRef.value.listPrompts(sessionId);
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Get a specific prompt
|
|
547
|
+
*/
|
|
548
|
+
const getPrompt = async (sessionId: string, name: string, args?: Record<string, string>): Promise<unknown> => {
|
|
549
|
+
if (!clientRef.value) {
|
|
550
|
+
throw new Error('SSE client not initialized');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return await clientRef.value.getPrompt(sessionId, name, args);
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* List resources
|
|
558
|
+
*/
|
|
559
|
+
const listResources = async (sessionId: string): Promise<ListResourcesResult> => {
|
|
560
|
+
if (!clientRef.value) {
|
|
561
|
+
throw new Error('SSE client not initialized');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return await clientRef.value.listResources(sessionId);
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Read a specific resource
|
|
569
|
+
*/
|
|
570
|
+
const readResource = async (sessionId: string, uri: string): Promise<unknown> => {
|
|
571
|
+
if (!clientRef.value) {
|
|
572
|
+
throw new Error('SSE client not initialized');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return await clientRef.value.readResource(sessionId, uri);
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
// Utility functions
|
|
579
|
+
const getConnection = (sessionId: string) => connections.value.find((c) => c.sessionId === sessionId);
|
|
580
|
+
|
|
581
|
+
const getConnectionByServerId = (serverId: string) => connections.value.find((c) => c.serverId === serverId);
|
|
582
|
+
|
|
583
|
+
const isServerConnected = (serverId: string) => {
|
|
584
|
+
const conn = getConnectionByServerId(serverId);
|
|
585
|
+
return conn ? conn.state === 'CONNECTED' || conn.state === 'DISCOVERING' || conn.state === 'READY' : false;
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const getTools = (sessionId: string) => {
|
|
589
|
+
const conn = getConnection(sessionId);
|
|
590
|
+
return conn?.tools || [];
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
// Return them as Ref objects so they can be destructured and stay reactive
|
|
595
|
+
connections: connections as unknown as { value: McpConnection[] },
|
|
596
|
+
status: status as unknown as { value: 'connecting' | 'connected' | 'disconnected' | 'error' },
|
|
597
|
+
isInitializing: isInitializing as unknown as { value: boolean },
|
|
598
|
+
connect,
|
|
599
|
+
disconnect,
|
|
600
|
+
getConnection,
|
|
601
|
+
getConnectionByServerId,
|
|
602
|
+
isServerConnected,
|
|
603
|
+
getTools,
|
|
604
|
+
refresh,
|
|
605
|
+
connectSSE,
|
|
606
|
+
disconnectSSE,
|
|
607
|
+
finishAuth,
|
|
608
|
+
resumeAuth,
|
|
609
|
+
callTool,
|
|
610
|
+
listTools,
|
|
611
|
+
listPrompts,
|
|
612
|
+
getPrompt,
|
|
613
|
+
listResources,
|
|
614
|
+
readResource,
|
|
615
|
+
sseClient: clientRef.value,
|
|
616
|
+
};
|
|
617
|
+
}
|