@mcp-ts/sdk 1.0.0 → 1.1.0

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 (77) hide show
  1. package/README.md +25 -13
  2. package/dist/adapters/agui-adapter.d.mts +21 -44
  3. package/dist/adapters/agui-adapter.d.ts +21 -44
  4. package/dist/adapters/agui-adapter.js +93 -67
  5. package/dist/adapters/agui-adapter.js.map +1 -1
  6. package/dist/adapters/agui-adapter.mjs +93 -68
  7. package/dist/adapters/agui-adapter.mjs.map +1 -1
  8. package/dist/adapters/agui-middleware.d.mts +32 -134
  9. package/dist/adapters/agui-middleware.d.ts +32 -134
  10. package/dist/adapters/agui-middleware.js +314 -350
  11. package/dist/adapters/agui-middleware.js.map +1 -1
  12. package/dist/adapters/agui-middleware.mjs +314 -351
  13. package/dist/adapters/agui-middleware.mjs.map +1 -1
  14. package/dist/adapters/ai-adapter.d.mts +2 -2
  15. package/dist/adapters/ai-adapter.d.ts +2 -2
  16. package/dist/adapters/langchain-adapter.d.mts +2 -2
  17. package/dist/adapters/langchain-adapter.d.ts +2 -2
  18. package/dist/adapters/mastra-adapter.d.mts +2 -2
  19. package/dist/adapters/mastra-adapter.d.ts +2 -2
  20. package/dist/client/index.d.mts +184 -57
  21. package/dist/client/index.d.ts +184 -57
  22. package/dist/client/index.js +535 -130
  23. package/dist/client/index.js.map +1 -1
  24. package/dist/client/index.mjs +535 -131
  25. package/dist/client/index.mjs.map +1 -1
  26. package/dist/client/react.d.mts +40 -6
  27. package/dist/client/react.d.ts +40 -6
  28. package/dist/client/react.js +587 -142
  29. package/dist/client/react.js.map +1 -1
  30. package/dist/client/react.mjs +586 -143
  31. package/dist/client/react.mjs.map +1 -1
  32. package/dist/client/vue.d.mts +5 -5
  33. package/dist/client/vue.d.ts +5 -5
  34. package/dist/client/vue.js +545 -140
  35. package/dist/client/vue.js.map +1 -1
  36. package/dist/client/vue.mjs +545 -141
  37. package/dist/client/vue.mjs.map +1 -1
  38. package/dist/{events-BP6WyRNh.d.mts → events-BgeztGYZ.d.mts} +12 -1
  39. package/dist/{events-BP6WyRNh.d.ts → events-BgeztGYZ.d.ts} +12 -1
  40. package/dist/index.d.mts +4 -4
  41. package/dist/index.d.ts +4 -4
  42. package/dist/index.js +779 -248
  43. package/dist/index.js.map +1 -1
  44. package/dist/index.mjs +775 -245
  45. package/dist/index.mjs.map +1 -1
  46. package/dist/{multi-session-client-DMF3ED2O.d.mts → multi-session-client-CxogNckF.d.mts} +1 -1
  47. package/dist/{multi-session-client-BOFgPypS.d.ts → multi-session-client-cox_WXUj.d.ts} +1 -1
  48. package/dist/server/index.d.mts +44 -40
  49. package/dist/server/index.d.ts +44 -40
  50. package/dist/server/index.js +242 -116
  51. package/dist/server/index.js.map +1 -1
  52. package/dist/server/index.mjs +238 -112
  53. package/dist/server/index.mjs.map +1 -1
  54. package/dist/shared/index.d.mts +2 -2
  55. package/dist/shared/index.d.ts +2 -2
  56. package/dist/shared/index.js.map +1 -1
  57. package/dist/shared/index.mjs.map +1 -1
  58. package/dist/{types-SbDlA2VX.d.mts → types-CLccx9wW.d.mts} +1 -1
  59. package/dist/{types-SbDlA2VX.d.ts → types-CLccx9wW.d.ts} +1 -1
  60. package/package.json +8 -1
  61. package/src/adapters/agui-adapter.ts +121 -107
  62. package/src/adapters/agui-middleware.ts +474 -512
  63. package/src/client/core/app-host.ts +417 -0
  64. package/src/client/core/sse-client.ts +365 -212
  65. package/src/client/core/types.ts +31 -0
  66. package/src/client/index.ts +1 -0
  67. package/src/client/react/index.ts +1 -0
  68. package/src/client/react/use-mcp-app.ts +73 -0
  69. package/src/client/react/useMcp.ts +18 -0
  70. package/src/server/handlers/nextjs-handler.ts +8 -7
  71. package/src/server/handlers/sse-handler.ts +131 -164
  72. package/src/server/mcp/oauth-client.ts +32 -2
  73. package/src/server/storage/index.ts +17 -1
  74. package/src/server/storage/sqlite-backend.ts +185 -0
  75. package/src/server/storage/types.ts +1 -1
  76. package/src/shared/events.ts +12 -0
  77. package/src/shared/types.ts +4 -2
@@ -1,10 +1,23 @@
1
1
  /**
2
2
  * SSE Client for MCP Connections
3
- * Browser-side client that connects to SSE endpoint
3
+ *
4
+ * Browser-side client that manages real-time communication with the MCP server
5
+ * using Server-Sent Events (SSE) for server→client streaming and HTTP POST for
6
+ * client→server RPC requests.
7
+ *
8
+ * Key features:
9
+ * - Direct HTTP response for RPC calls (bypasses SSE latency)
10
+ * - Resource preloading for instant MCP App UI loading
11
+ * - Automatic reconnection with exponential backoff
12
+ * - Type-safe RPC methods
4
13
  */
5
14
 
6
15
  import { nanoid } from 'nanoid';
7
- import type { McpConnectionEvent, McpObservabilityEvent } from '../../shared/events';
16
+ import type {
17
+ McpConnectionEvent,
18
+ McpObservabilityEvent,
19
+ McpAppsUIEvent
20
+ } from '../../shared/events.js';
8
21
  import type {
9
22
  McpRpcRequest,
10
23
  McpRpcResponse,
@@ -19,60 +32,87 @@ import type {
19
32
  ListToolsRpcResult,
20
33
  ListPromptsResult,
21
34
  ListResourcesResult,
22
- } from '../../shared/types';
35
+ } from '../../shared/types.js';
36
+ // ============================================
37
+ // Types & Interfaces
38
+ // ============================================
23
39
 
24
40
  export interface SSEClientOptions {
25
- /**
26
- * SSE endpoint URL
27
- */
41
+ /** SSE endpoint URL */
28
42
  url: string;
29
43
 
30
- /**
31
- * User/Client identifier
32
- */
44
+ /** User/Client identifier */
33
45
  identity: string;
34
46
 
35
- /**
36
- * Optional auth token
37
- */
47
+ /** Optional auth token for authenticated requests */
38
48
  authToken?: string;
39
49
 
40
- /**
41
- * Connection event callback
42
- */
50
+ /** Callback for MCP connection state changes */
43
51
  onConnectionEvent?: (event: McpConnectionEvent) => void;
44
52
 
45
- /**
46
- * Observability event callback
47
- */
53
+ /** Callback for observability/logging events */
48
54
  onObservabilityEvent?: (event: McpObservabilityEvent) => void;
49
55
 
50
- /**
51
- * Connection status callback
52
- */
53
- onStatusChange?: (status: 'connecting' | 'connected' | 'disconnected' | 'error') => void;
56
+ /** Callback for connection status changes */
57
+ onStatusChange?: (status: ConnectionStatus) => void;
58
+
59
+ /** Callback for MCP App UI events */
60
+ onEvent?: (event: McpAppsUIEvent) => void;
61
+
62
+ /** Request timeout in milliseconds @default 60000 */
63
+ requestTimeout?: number;
64
+
65
+ /** Enable debug logging @default false */
66
+ debug?: boolean;
67
+ }
68
+
69
+ export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
70
+
71
+ interface PendingRequest {
72
+ resolve: (value: unknown) => void;
73
+ reject: (error: Error) => void;
74
+ timeoutId: ReturnType<typeof setTimeout>;
54
75
  }
55
76
 
77
+ interface ToolUiMetadata {
78
+ resourceUri?: string;
79
+ uri?: string;
80
+ visibility?: string[];
81
+ }
82
+
83
+ // ============================================
84
+ // Constants
85
+ // ============================================
86
+
87
+ const DEFAULT_REQUEST_TIMEOUT = 60000;
88
+ const MAX_RECONNECT_ATTEMPTS = 5;
89
+ const BASE_RECONNECT_DELAY = 1000;
90
+
91
+ // ============================================
92
+ // SSEClient Class
93
+ // ============================================
94
+
56
95
  /**
57
96
  * SSE Client for real-time MCP connection management
58
97
  */
59
98
  export class SSEClient {
60
99
  private eventSource: EventSource | null = null;
61
- private pendingRequests: Map<
62
- string,
63
- { resolve: (value: unknown) => void; reject: (error: Error) => void }
64
- > = new Map();
65
- private reconnectAttempts: number = 0;
66
- private maxReconnectAttempts: number = 5;
67
- private reconnectDelay: number = 1000;
68
- private isManuallyDisconnected: boolean = false;
100
+ private pendingRequests = new Map<string, PendingRequest>();
101
+ private resourceCache = new Map<string, Promise<unknown>>();
102
+
103
+ private reconnectAttempts = 0;
104
+ private isManuallyDisconnected = false;
69
105
  private connectionPromise: Promise<void> | null = null;
70
106
  private connectionResolver: (() => void) | null = null;
71
107
 
72
- constructor(private options: SSEClientOptions) { }
108
+ constructor(private readonly options: SSEClientOptions) {}
109
+
110
+ // ============================================
111
+ // Connection Management
112
+ // ============================================
73
113
 
74
114
  /**
75
- * Connect to SSE endpoint
115
+ * Connect to the SSE endpoint
76
116
  */
77
117
  connect(): void {
78
118
  if (this.eventSource) {
@@ -81,80 +121,17 @@ export class SSEClient {
81
121
 
82
122
  this.isManuallyDisconnected = false;
83
123
  this.options.onStatusChange?.('connecting');
84
-
85
- // Create connection promise
86
124
  this.connectionPromise = new Promise((resolve) => {
87
125
  this.connectionResolver = resolve;
88
126
  });
89
127
 
90
- // Build URL with query params
91
- // Handle both relative and absolute URLs
92
- const url = new URL(this.options.url, typeof window !== 'undefined' ? window.location.origin : undefined);
93
- url.searchParams.set('identity', this.options.identity);
94
- if (this.options.authToken) {
95
- url.searchParams.set('token', this.options.authToken);
96
- }
97
-
98
- // Create EventSource
99
- this.eventSource = new EventSource(url.toString());
100
-
101
- // Handle connection open
102
- this.eventSource.addEventListener('open', () => {
103
- console.log('[SSEClient] Connected');
104
- this.reconnectAttempts = 0;
105
- this.options.onStatusChange?.('connected');
106
- });
107
-
108
- // Handle 'connected' event - server confirms manager is ready
109
- this.eventSource.addEventListener('connected', (e: MessageEvent) => {
110
- const data = JSON.parse(e.data);
111
- console.log('[SSEClient] Server ready:', data);
112
-
113
- // Resolve connection promise - now safe to send requests
114
- if (this.connectionResolver) {
115
- this.connectionResolver();
116
- this.connectionResolver = null;
117
- }
118
- });
119
-
120
- // Handle 'connection' events (MCP connection state changes)
121
- this.eventSource.addEventListener('connection', (e: MessageEvent) => {
122
- const event: McpConnectionEvent = JSON.parse(e.data);
123
- this.options.onConnectionEvent?.(event);
124
- });
125
-
126
- // Handle 'observability' events (debugging/logging)
127
- this.eventSource.addEventListener('observability', (e: MessageEvent) => {
128
- const event: McpObservabilityEvent = JSON.parse(e.data);
129
- this.options.onObservabilityEvent?.(event);
130
- });
131
-
132
- // Handle 'rpc-response' events (RPC method responses)
133
- this.eventSource.addEventListener('rpc-response', (e: MessageEvent) => {
134
- const response: McpRpcResponse = JSON.parse(e.data);
135
- this.handleRpcResponse(response);
136
- });
137
-
138
- // Handle errors
139
- this.eventSource.addEventListener('error', () => {
140
- console.error('[SSEClient] Connection error');
141
- this.options.onStatusChange?.('error');
142
-
143
- // Attempt reconnection
144
- if (!this.isManuallyDisconnected && this.reconnectAttempts < this.maxReconnectAttempts) {
145
- this.reconnectAttempts++;
146
- console.log(`[SSEClient] Reconnecting (attempt ${this.reconnectAttempts})...`);
147
-
148
- setTimeout(() => {
149
- this.disconnect();
150
- this.connect();
151
- }, this.reconnectDelay * this.reconnectAttempts);
152
- }
153
- });
128
+ const url = this.buildUrl();
129
+ this.eventSource = new EventSource(url);
130
+ this.setupEventListeners();
154
131
  }
155
132
 
156
133
  /**
157
- * Disconnect from SSE endpoint
134
+ * Disconnect from the SSE endpoint
158
135
  */
159
136
  disconnect(): void {
160
137
  this.isManuallyDisconnected = true;
@@ -164,177 +141,353 @@ export class SSEClient {
164
141
  this.eventSource = null;
165
142
  }
166
143
 
167
- // Reset connection promise
168
144
  this.connectionPromise = null;
169
145
  this.connectionResolver = null;
170
-
171
- // Reject all pending requests with a specific error type
172
- for (const [id, { reject }] of this.pendingRequests.entries()) {
173
- const error = new Error('Connection closed');
174
- error.name = 'ConnectionClosedError';
175
- reject(error);
176
- }
177
- this.pendingRequests.clear();
178
-
146
+ this.rejectAllPendingRequests(new Error('Connection closed'));
179
147
  this.options.onStatusChange?.('disconnected');
180
148
  }
181
149
 
182
150
  /**
183
- * Send RPC request via SSE
184
- * Note: SSE is unidirectional (server->client), so we need to send requests via POST
151
+ * Check if connected to the SSE endpoint
185
152
  */
186
- private async sendRequest<T = unknown>(method: McpRpcMethod, params?: McpRpcParams): Promise<T> {
187
- // Wait for connection to be fully established
188
- if (this.connectionPromise) {
189
- await this.connectionPromise;
190
- }
191
-
192
- // Generate unique request ID using nanoid (e.g., "rpc_V1StGXR8_Z5jdHi")
193
- const id = `rpc_${nanoid(10)}`;
194
-
195
- const request: McpRpcRequest = {
196
- id,
197
- method,
198
- params,
199
- };
200
-
201
- // Create promise for response
202
- const promise = new Promise<T>((resolve, reject) => {
203
- this.pendingRequests.set(id, { resolve: resolve as (value: unknown) => void, reject });
204
-
205
- // Timeout after 30 seconds
206
- setTimeout(() => {
207
- if (this.pendingRequests.has(id)) {
208
- this.pendingRequests.delete(id);
209
- reject(new Error('Request timeout'));
210
- }
211
- }, 30000);
212
- });
213
-
214
- // Send request via POST to same endpoint
215
- try {
216
- // Handle both relative and absolute URLs
217
- const url = new URL(this.options.url, typeof window !== 'undefined' ? window.location.origin : undefined);
218
- url.searchParams.set('identity', this.options.identity);
219
-
220
- await fetch(url.toString(), {
221
- method: 'POST',
222
- headers: {
223
- 'Content-Type': 'application/json',
224
- ...(this.options.authToken && { Authorization: `Bearer ${this.options.authToken}` }),
225
- },
226
- body: JSON.stringify(request),
227
- });
228
- } catch (error) {
229
- this.pendingRequests.delete(id);
230
- throw error;
231
- }
232
-
233
- return promise;
153
+ isConnected(): boolean {
154
+ return this.eventSource?.readyState === EventSource.OPEN;
234
155
  }
235
156
 
236
- /**
237
- * Handle RPC response
238
- */
239
- private handleRpcResponse(response: McpRpcResponse): void {
240
- const pending = this.pendingRequests.get(response.id);
157
+ // ============================================
158
+ // RPC Methods
159
+ // ============================================
241
160
 
242
- if (pending) {
243
- this.pendingRequests.delete(response.id);
244
-
245
- if (response.error) {
246
- pending.reject(new Error(response.error.message));
247
- } else {
248
- pending.resolve(response.result);
249
- }
250
- }
251
- }
252
-
253
- /**
254
- * Get all user sessions
255
- */
256
161
  async getSessions(): Promise<SessionListResult> {
257
162
  return this.sendRequest<SessionListResult>('getSessions');
258
163
  }
259
164
 
260
- /**
261
- * Connect to an MCP server
262
- */
263
165
  async connectToServer(params: ConnectParams): Promise<ConnectResult> {
264
166
  return this.sendRequest<ConnectResult>('connect', params);
265
167
  }
266
168
 
267
- /**
268
- * Disconnect from an MCP server
269
- */
270
169
  async disconnectFromServer(sessionId: string): Promise<DisconnectResult> {
271
170
  return this.sendRequest<DisconnectResult>('disconnect', { sessionId });
272
171
  }
273
172
 
274
- /**
275
- * List tools from a session
276
- */
277
173
  async listTools(sessionId: string): Promise<ListToolsRpcResult> {
278
174
  return this.sendRequest<ListToolsRpcResult>('listTools', { sessionId });
279
175
  }
280
176
 
281
- /**
282
- * Call a tool
283
- */
284
177
  async callTool(
285
178
  sessionId: string,
286
179
  toolName: string,
287
180
  toolArgs: Record<string, unknown>
288
181
  ): Promise<unknown> {
289
- return this.sendRequest('callTool', { sessionId, toolName, toolArgs });
182
+ const result = await this.sendRequest('callTool', { sessionId, toolName, toolArgs });
183
+ this.emitUiEventIfPresent(result, sessionId, toolName);
184
+ return result;
290
185
  }
291
186
 
292
- /**
293
- * Refresh/validate a session
294
- */
295
187
  async restoreSession(sessionId: string): Promise<RestoreSessionResult> {
296
188
  return this.sendRequest<RestoreSessionResult>('restoreSession', { sessionId });
297
189
  }
298
190
 
299
- /**
300
- * Complete OAuth authorization
301
- */
302
191
  async finishAuth(sessionId: string, code: string): Promise<FinishAuthResult> {
303
192
  return this.sendRequest<FinishAuthResult>('finishAuth', { sessionId, code });
304
193
  }
305
194
 
306
- /**
307
- * List available prompts
308
- */
309
195
  async listPrompts(sessionId: string): Promise<ListPromptsResult> {
310
196
  return this.sendRequest<ListPromptsResult>('listPrompts', { sessionId });
311
197
  }
312
198
 
313
- /**
314
- * Get a specific prompt with arguments
315
- */
316
199
  async getPrompt(sessionId: string, name: string, args?: Record<string, string>): Promise<unknown> {
317
200
  return this.sendRequest('getPrompt', { sessionId, name, args });
318
201
  }
319
202
 
320
- /**
321
- * List available resources
322
- */
323
203
  async listResources(sessionId: string): Promise<ListResourcesResult> {
324
204
  return this.sendRequest<ListResourcesResult>('listResources', { sessionId });
325
205
  }
326
206
 
327
- /**
328
- * Read a specific resource
329
- */
330
207
  async readResource(sessionId: string, uri: string): Promise<unknown> {
331
208
  return this.sendRequest('readResource', { sessionId, uri });
332
209
  }
333
210
 
211
+ // ============================================
212
+ // Resource Preloading (for instant UI loading)
213
+ // ============================================
214
+
334
215
  /**
335
- * Check if connected
216
+ * Preload UI resources for tools that have UI metadata.
217
+ * Call this when tools are discovered to enable instant MCP App UI loading.
336
218
  */
337
- isConnected(): boolean {
338
- return this.eventSource !== null && this.eventSource.readyState === EventSource.OPEN;
219
+ preloadToolUiResources(sessionId: string, tools: Array<{ name: string; _meta?: unknown }>): void {
220
+ for (const tool of tools) {
221
+ const uri = this.extractUiResourceUri(tool);
222
+ if (!uri) continue;
223
+
224
+ if (this.resourceCache.has(uri)) {
225
+ this.log(`Resource already cached: ${uri}`);
226
+ continue;
227
+ }
228
+
229
+ this.log(`Preloading UI resource for tool "${tool.name}": ${uri}`);
230
+ const promise = this.sendRequest('readResource', { sessionId, uri })
231
+ .catch((err) => {
232
+ this.log(`Failed to preload resource ${uri}: ${err.message}`, 'warn');
233
+ this.resourceCache.delete(uri);
234
+ return null;
235
+ });
236
+
237
+ this.resourceCache.set(uri, promise);
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Get a preloaded resource from cache, or fetch if not cached.
243
+ */
244
+ getOrFetchResource(sessionId: string, uri: string): Promise<unknown> {
245
+ const cached = this.resourceCache.get(uri);
246
+ if (cached) {
247
+ this.log(`Cache hit for resource: ${uri}`);
248
+ return cached;
249
+ }
250
+
251
+ this.log(`Cache miss, fetching resource: ${uri}`);
252
+ const promise = this.sendRequest('readResource', { sessionId, uri });
253
+ this.resourceCache.set(uri, promise);
254
+ return promise;
255
+ }
256
+
257
+ /**
258
+ * Check if a resource is already cached
259
+ */
260
+ hasPreloadedResource(uri: string): boolean {
261
+ return this.resourceCache.has(uri);
262
+ }
263
+
264
+ /**
265
+ * Clear the resource cache
266
+ */
267
+ clearResourceCache(): void {
268
+ this.resourceCache.clear();
269
+ }
270
+
271
+ // ============================================
272
+ // Private: Request Handling
273
+ // ============================================
274
+
275
+ /**
276
+ * Send an RPC request and return the response directly from HTTP.
277
+ * This bypasses SSE latency by returning results in the HTTP response body.
278
+ */
279
+ private async sendRequest<T = unknown>(method: McpRpcMethod, params?: McpRpcParams): Promise<T> {
280
+ if (this.connectionPromise) {
281
+ await this.connectionPromise;
282
+ }
283
+
284
+ const request: McpRpcRequest = {
285
+ id: `rpc_${nanoid(10)}`,
286
+ method,
287
+ params,
288
+ };
289
+
290
+ const response = await fetch(this.buildUrl(), {
291
+ method: 'POST',
292
+ headers: this.buildHeaders(),
293
+ body: JSON.stringify(request),
294
+ });
295
+
296
+ if (!response.ok) {
297
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
298
+ }
299
+
300
+ const data = await response.json() as McpRpcResponse;
301
+ return this.parseRpcResponse<T>(data, request.id);
302
+ }
303
+
304
+ /**
305
+ * Parse RPC response and handle different response formats
306
+ */
307
+ private parseRpcResponse<T>(data: McpRpcResponse, requestId: string): T | Promise<T> {
308
+ // Fast path: Direct response (new behavior)
309
+ if ('result' in data) {
310
+ return data.result as T;
311
+ }
312
+
313
+ // Error response
314
+ if ('error' in data && data.error) {
315
+ throw new Error(data.error.message || 'Unknown RPC error');
316
+ }
317
+
318
+ // Legacy path: Acknowledgment only (wait for SSE)
319
+ // Kept for backwards compatibility with older servers
320
+ if ('acknowledged' in data) {
321
+ return this.waitForSseResponse<T>(requestId);
322
+ }
323
+
324
+ throw new Error('Invalid RPC response format');
325
+ }
326
+
327
+ /**
328
+ * Wait for RPC response via SSE (legacy fallback)
329
+ */
330
+ private waitForSseResponse<T>(requestId: string): Promise<T> {
331
+ const timeoutMs = this.options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
332
+
333
+ return new Promise<T>((resolve, reject) => {
334
+ const timeoutId = setTimeout(() => {
335
+ this.pendingRequests.delete(requestId);
336
+ reject(new Error(`Request timeout after ${timeoutMs}ms`));
337
+ }, timeoutMs);
338
+
339
+ this.pendingRequests.set(requestId, {
340
+ resolve: resolve as (value: unknown) => void,
341
+ reject,
342
+ timeoutId,
343
+ });
344
+ });
345
+ }
346
+
347
+ /**
348
+ * Handle RPC response received via SSE (legacy)
349
+ */
350
+ private handleRpcResponse(response: McpRpcResponse): void {
351
+ const pending = this.pendingRequests.get(response.id);
352
+ if (!pending) return;
353
+
354
+ clearTimeout(pending.timeoutId);
355
+ this.pendingRequests.delete(response.id);
356
+
357
+ if (response.error) {
358
+ pending.reject(new Error(response.error.message));
359
+ } else {
360
+ pending.resolve(response.result);
361
+ }
362
+ }
363
+
364
+ // ============================================
365
+ // Private: Event Handling
366
+ // ============================================
367
+
368
+ private setupEventListeners(): void {
369
+ if (!this.eventSource) return;
370
+
371
+ this.eventSource.addEventListener('open', () => {
372
+ this.log('Connected');
373
+ this.reconnectAttempts = 0;
374
+ this.options.onStatusChange?.('connected');
375
+ });
376
+
377
+ this.eventSource.addEventListener('connected', () => {
378
+ this.log('Server ready');
379
+ this.connectionResolver?.();
380
+ this.connectionResolver = null;
381
+ });
382
+
383
+ this.eventSource.addEventListener('connection', (e: MessageEvent) => {
384
+ const event = JSON.parse(e.data) as McpConnectionEvent;
385
+ this.options.onConnectionEvent?.(event);
386
+ });
387
+
388
+ this.eventSource.addEventListener('observability', (e: MessageEvent) => {
389
+ const event = JSON.parse(e.data) as McpObservabilityEvent;
390
+ this.options.onObservabilityEvent?.(event);
391
+ });
392
+
393
+ this.eventSource.addEventListener('rpc-response', (e: MessageEvent) => {
394
+ const response = JSON.parse(e.data) as McpRpcResponse;
395
+ this.handleRpcResponse(response);
396
+ });
397
+
398
+ this.eventSource.addEventListener('error', () => {
399
+ this.log('Connection error', 'error');
400
+ this.options.onStatusChange?.('error');
401
+ this.attemptReconnect();
402
+ });
403
+ }
404
+
405
+ private attemptReconnect(): void {
406
+ if (this.isManuallyDisconnected || this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
407
+ return;
408
+ }
409
+
410
+ this.reconnectAttempts++;
411
+ const delay = BASE_RECONNECT_DELAY * this.reconnectAttempts;
412
+ this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
413
+
414
+ setTimeout(() => {
415
+ this.disconnect();
416
+ this.connect();
417
+ }, delay);
418
+ }
419
+
420
+ // ============================================
421
+ // Private: Utilities
422
+ // ============================================
423
+
424
+ private buildUrl(): string {
425
+ const url = new URL(this.options.url, globalThis.location?.origin);
426
+ url.searchParams.set('identity', this.options.identity);
427
+ if (this.options.authToken) {
428
+ url.searchParams.set('token', this.options.authToken);
429
+ }
430
+ return url.toString();
431
+ }
432
+
433
+ private buildHeaders(): HeadersInit {
434
+ const headers: HeadersInit = {
435
+ 'Content-Type': 'application/json',
436
+ };
437
+ if (this.options.authToken) {
438
+ headers['Authorization'] = `Bearer ${this.options.authToken}`;
439
+ }
440
+ return headers;
441
+ }
442
+
443
+ private rejectAllPendingRequests(error: Error): void {
444
+ for (const [, pending] of this.pendingRequests) {
445
+ clearTimeout(pending.timeoutId);
446
+ pending.reject(error);
447
+ }
448
+ this.pendingRequests.clear();
449
+ }
450
+
451
+ private extractUiResourceUri(tool: { name: string; _meta?: unknown }): string | undefined {
452
+ const meta = (tool._meta as { ui?: ToolUiMetadata })?.ui;
453
+ if (!meta || typeof meta !== 'object') return undefined;
454
+
455
+ // Check visibility constraint
456
+ if (meta.visibility && !meta.visibility.includes('app')) return undefined;
457
+
458
+ // Support both 'resourceUri' and 'uri' field names
459
+ return meta.resourceUri ?? meta.uri;
460
+ }
461
+
462
+ private emitUiEventIfPresent(result: unknown, sessionId: string, toolName: string): void {
463
+ const meta = (result as { _meta?: { ui?: ToolUiMetadata } })?._meta;
464
+ const resourceUri = meta?.ui?.resourceUri ?? (meta as any)?.['ui/resourceUri'];
465
+
466
+ if (resourceUri) {
467
+ this.options.onEvent?.({
468
+ type: 'mcp-apps-ui',
469
+ sessionId,
470
+ resourceUri,
471
+ toolName,
472
+ result,
473
+ timestamp: Date.now(),
474
+ });
475
+ }
476
+ }
477
+
478
+ private log(message: string, level: 'info' | 'warn' | 'error' = 'info'): void {
479
+ if (!this.options.debug && level === 'info') return;
480
+
481
+ const prefix = '[SSEClient]';
482
+ switch (level) {
483
+ case 'warn':
484
+ console.warn(prefix, message);
485
+ break;
486
+ case 'error':
487
+ console.error(prefix, message);
488
+ break;
489
+ default:
490
+ console.log(prefix, message);
491
+ }
339
492
  }
340
493
  }