@mcp-ts/sdk 1.3.6 → 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.
Files changed (103) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +398 -404
  3. package/dist/adapters/agui-adapter.d.mts +1 -1
  4. package/dist/adapters/agui-adapter.d.ts +1 -1
  5. package/dist/adapters/agui-adapter.js +2 -2
  6. package/dist/adapters/agui-adapter.js.map +1 -1
  7. package/dist/adapters/agui-adapter.mjs +2 -2
  8. package/dist/adapters/agui-adapter.mjs.map +1 -1
  9. package/dist/adapters/agui-middleware.d.mts +1 -1
  10. package/dist/adapters/agui-middleware.d.ts +1 -1
  11. package/dist/adapters/agui-middleware.js.map +1 -1
  12. package/dist/adapters/agui-middleware.mjs.map +1 -1
  13. package/dist/adapters/ai-adapter.d.mts +1 -1
  14. package/dist/adapters/ai-adapter.d.ts +1 -1
  15. package/dist/adapters/ai-adapter.js +1 -1
  16. package/dist/adapters/ai-adapter.js.map +1 -1
  17. package/dist/adapters/ai-adapter.mjs +1 -1
  18. package/dist/adapters/ai-adapter.mjs.map +1 -1
  19. package/dist/adapters/langchain-adapter.d.mts +1 -1
  20. package/dist/adapters/langchain-adapter.d.ts +1 -1
  21. package/dist/adapters/langchain-adapter.js +1 -1
  22. package/dist/adapters/langchain-adapter.js.map +1 -1
  23. package/dist/adapters/langchain-adapter.mjs +1 -1
  24. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  25. package/dist/adapters/mastra-adapter.d.mts +1 -1
  26. package/dist/adapters/mastra-adapter.d.ts +1 -1
  27. package/dist/adapters/mastra-adapter.js +1 -1
  28. package/dist/adapters/mastra-adapter.js.map +1 -1
  29. package/dist/adapters/mastra-adapter.mjs +1 -1
  30. package/dist/adapters/mastra-adapter.mjs.map +1 -1
  31. package/dist/bin/mcp-ts.js +0 -0
  32. package/dist/bin/mcp-ts.js.map +1 -1
  33. package/dist/bin/mcp-ts.mjs +0 -0
  34. package/dist/bin/mcp-ts.mjs.map +1 -1
  35. package/dist/client/index.js.map +1 -1
  36. package/dist/client/index.mjs.map +1 -1
  37. package/dist/client/react.d.mts +2 -2
  38. package/dist/client/react.d.ts +2 -2
  39. package/dist/client/react.js +25 -2
  40. package/dist/client/react.js.map +1 -1
  41. package/dist/client/react.mjs +26 -3
  42. package/dist/client/react.mjs.map +1 -1
  43. package/dist/client/vue.js.map +1 -1
  44. package/dist/client/vue.mjs.map +1 -1
  45. package/dist/index.d.mts +1 -1
  46. package/dist/index.d.ts +1 -1
  47. package/dist/index.js +134 -71
  48. package/dist/index.js.map +1 -1
  49. package/dist/index.mjs +134 -71
  50. package/dist/index.mjs.map +1 -1
  51. package/dist/{multi-session-client-BYLarghq.d.ts → multi-session-client-CHE8QpVE.d.ts} +75 -5
  52. package/dist/{multi-session-client-CzhMkE0k.d.mts → multi-session-client-CQsRbxYI.d.mts} +75 -5
  53. package/dist/server/index.d.mts +1 -1
  54. package/dist/server/index.d.ts +1 -1
  55. package/dist/server/index.js +134 -71
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/index.mjs +134 -71
  58. package/dist/server/index.mjs.map +1 -1
  59. package/dist/shared/index.js +10 -2
  60. package/dist/shared/index.js.map +1 -1
  61. package/dist/shared/index.mjs +10 -2
  62. package/dist/shared/index.mjs.map +1 -1
  63. package/package.json +185 -185
  64. package/src/adapters/agui-adapter.ts +222 -222
  65. package/src/adapters/agui-middleware.ts +382 -382
  66. package/src/adapters/ai-adapter.ts +115 -115
  67. package/src/adapters/langchain-adapter.ts +127 -127
  68. package/src/adapters/mastra-adapter.ts +126 -126
  69. package/src/bin/mcp-ts.ts +102 -102
  70. package/src/client/core/app-host.ts +417 -417
  71. package/src/client/core/sse-client.ts +371 -371
  72. package/src/client/core/types.ts +31 -31
  73. package/src/client/index.ts +27 -27
  74. package/src/client/react/index.ts +16 -16
  75. package/src/client/react/use-app-host.ts +73 -73
  76. package/src/client/react/use-mcp-apps.tsx +247 -214
  77. package/src/client/react/use-mcp.ts +641 -641
  78. package/src/client/vue/index.ts +10 -10
  79. package/src/client/vue/use-mcp.ts +617 -617
  80. package/src/index.ts +11 -11
  81. package/src/server/handlers/nextjs-handler.ts +204 -204
  82. package/src/server/handlers/sse-handler.ts +631 -631
  83. package/src/server/index.ts +57 -57
  84. package/src/server/mcp/multi-session-client.ts +228 -132
  85. package/src/server/mcp/oauth-client.ts +1188 -1188
  86. package/src/server/mcp/storage-oauth-provider.ts +272 -272
  87. package/src/server/storage/file-backend.ts +157 -170
  88. package/src/server/storage/index.ts +176 -175
  89. package/src/server/storage/memory-backend.ts +123 -136
  90. package/src/server/storage/redis-backend.ts +276 -289
  91. package/src/server/storage/redis.ts +160 -160
  92. package/src/server/storage/sqlite-backend.ts +182 -186
  93. package/src/server/storage/supabase-backend.ts +228 -227
  94. package/src/server/storage/types.ts +116 -116
  95. package/src/shared/constants.ts +29 -29
  96. package/src/shared/errors.ts +133 -133
  97. package/src/shared/event-routing.ts +28 -28
  98. package/src/shared/events.ts +180 -180
  99. package/src/shared/index.ts +75 -75
  100. package/src/shared/tool-utils.ts +61 -61
  101. package/src/shared/types.ts +282 -282
  102. package/src/shared/utils.ts +38 -16
  103. 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
+ }