@mcp-ts/sdk 1.3.2 → 1.3.4

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 (58) hide show
  1. package/README.md +400 -406
  2. package/dist/adapters/agui-adapter.d.mts +1 -1
  3. package/dist/adapters/agui-adapter.d.ts +1 -1
  4. package/dist/adapters/agui-middleware.d.mts +1 -1
  5. package/dist/adapters/agui-middleware.d.ts +1 -1
  6. package/dist/adapters/ai-adapter.d.mts +1 -1
  7. package/dist/adapters/ai-adapter.d.ts +1 -1
  8. package/dist/adapters/langchain-adapter.d.mts +1 -1
  9. package/dist/adapters/langchain-adapter.d.ts +1 -1
  10. package/dist/adapters/mastra-adapter.d.mts +1 -1
  11. package/dist/adapters/mastra-adapter.d.ts +1 -1
  12. package/dist/client/index.d.mts +8 -64
  13. package/dist/client/index.d.ts +8 -64
  14. package/dist/client/index.js +91 -173
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/index.mjs +91 -173
  17. package/dist/client/index.mjs.map +1 -1
  18. package/dist/client/react.d.mts +12 -2
  19. package/dist/client/react.d.ts +12 -2
  20. package/dist/client/react.js +119 -182
  21. package/dist/client/react.js.map +1 -1
  22. package/dist/client/react.mjs +119 -182
  23. package/dist/client/react.mjs.map +1 -1
  24. package/dist/client/vue.d.mts +24 -4
  25. package/dist/client/vue.d.ts +24 -4
  26. package/dist/client/vue.js +121 -182
  27. package/dist/client/vue.js.map +1 -1
  28. package/dist/client/vue.mjs +121 -182
  29. package/dist/client/vue.mjs.map +1 -1
  30. package/dist/index.d.mts +2 -2
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +215 -250
  33. package/dist/index.js.map +1 -1
  34. package/dist/index.mjs +215 -250
  35. package/dist/index.mjs.map +1 -1
  36. package/dist/{multi-session-client-B1DBx5yR.d.mts → multi-session-client-DzjmT7FX.d.mts} +1 -0
  37. package/dist/{multi-session-client-DyFzyJUx.d.ts → multi-session-client-FAFpUzZ4.d.ts} +1 -0
  38. package/dist/server/index.d.mts +16 -21
  39. package/dist/server/index.d.ts +16 -21
  40. package/dist/server/index.js +124 -77
  41. package/dist/server/index.js.map +1 -1
  42. package/dist/server/index.mjs +124 -77
  43. package/dist/server/index.mjs.map +1 -1
  44. package/dist/shared/index.d.mts +2 -2
  45. package/dist/shared/index.d.ts +2 -2
  46. package/dist/shared/index.js.map +1 -1
  47. package/dist/shared/index.mjs.map +1 -1
  48. package/dist/{types-PjM1W07s.d.mts → types-CW6lghof.d.mts} +5 -0
  49. package/dist/{types-PjM1W07s.d.ts → types-CW6lghof.d.ts} +5 -0
  50. package/package.json +1 -1
  51. package/src/client/core/sse-client.ts +354 -493
  52. package/src/client/react/use-mcp.ts +75 -23
  53. package/src/client/vue/use-mcp.ts +111 -48
  54. package/src/server/handlers/nextjs-handler.ts +207 -217
  55. package/src/server/handlers/sse-handler.ts +10 -0
  56. package/src/server/mcp/oauth-client.ts +41 -32
  57. package/src/server/storage/types.ts +12 -5
  58. package/src/shared/types.ts +5 -0
@@ -1,493 +1,354 @@
1
- /**
2
- * SSE Client for MCP Connections
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
13
- */
14
-
15
- import { nanoid } from 'nanoid';
16
- import type {
17
- McpConnectionEvent,
18
- McpObservabilityEvent,
19
- McpAppsUIEvent
20
- } from '../../shared/events.js';
21
- import type {
22
- McpRpcRequest,
23
- McpRpcResponse,
24
- McpRpcMethod,
25
- McpRpcParams,
26
- ConnectParams,
27
- SessionListResult,
28
- ConnectResult,
29
- DisconnectResult,
30
- RestoreSessionResult,
31
- FinishAuthResult,
32
- ListToolsRpcResult,
33
- ListPromptsResult,
34
- ListResourcesResult,
35
- } from '../../shared/types.js';
36
- // ============================================
37
- // Types & Interfaces
38
- // ============================================
39
-
40
- export interface SSEClientOptions {
41
- /** SSE endpoint URL */
42
- url: string;
43
-
44
- /** User/Client identifier */
45
- identity: string;
46
-
47
- /** Optional auth token for authenticated requests */
48
- authToken?: string;
49
-
50
- /** Callback for MCP connection state changes */
51
- onConnectionEvent?: (event: McpConnectionEvent) => void;
52
-
53
- /** Callback for observability/logging events */
54
- onObservabilityEvent?: (event: McpObservabilityEvent) => void;
55
-
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>;
75
- }
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
-
95
- /**
96
- * SSE Client for real-time MCP connection management
97
- */
98
- export class SSEClient {
99
- private eventSource: EventSource | null = null;
100
- private pendingRequests = new Map<string, PendingRequest>();
101
- private resourceCache = new Map<string, Promise<unknown>>();
102
-
103
- private reconnectAttempts = 0;
104
- private isManuallyDisconnected = false;
105
- private connectionPromise: Promise<void> | null = null;
106
- private connectionResolver: (() => void) | null = null;
107
-
108
- constructor(private readonly options: SSEClientOptions) {}
109
-
110
- // ============================================
111
- // Connection Management
112
- // ============================================
113
-
114
- /**
115
- * Connect to the SSE endpoint
116
- */
117
- connect(): void {
118
- if (this.eventSource) {
119
- return; // Already connected
120
- }
121
-
122
- this.isManuallyDisconnected = false;
123
- this.options.onStatusChange?.('connecting');
124
- this.connectionPromise = new Promise((resolve) => {
125
- this.connectionResolver = resolve;
126
- });
127
-
128
- const url = this.buildUrl();
129
- this.eventSource = new EventSource(url);
130
- this.setupEventListeners();
131
- }
132
-
133
- /**
134
- * Disconnect from the SSE endpoint
135
- */
136
- disconnect(): void {
137
- this.isManuallyDisconnected = true;
138
-
139
- if (this.eventSource) {
140
- this.eventSource.close();
141
- this.eventSource = null;
142
- }
143
-
144
- this.connectionPromise = null;
145
- this.connectionResolver = null;
146
- this.rejectAllPendingRequests(new Error('Connection closed'));
147
- this.options.onStatusChange?.('disconnected');
148
- }
149
-
150
- /**
151
- * Check if connected to the SSE endpoint
152
- */
153
- isConnected(): boolean {
154
- return this.eventSource?.readyState === EventSource.OPEN;
155
- }
156
-
157
- // ============================================
158
- // RPC Methods
159
- // ============================================
160
-
161
- async getSessions(): Promise<SessionListResult> {
162
- return this.sendRequest<SessionListResult>('getSessions');
163
- }
164
-
165
- async connectToServer(params: ConnectParams): Promise<ConnectResult> {
166
- return this.sendRequest<ConnectResult>('connect', params);
167
- }
168
-
169
- async disconnectFromServer(sessionId: string): Promise<DisconnectResult> {
170
- return this.sendRequest<DisconnectResult>('disconnect', { sessionId });
171
- }
172
-
173
- async listTools(sessionId: string): Promise<ListToolsRpcResult> {
174
- return this.sendRequest<ListToolsRpcResult>('listTools', { sessionId });
175
- }
176
-
177
- async callTool(
178
- sessionId: string,
179
- toolName: string,
180
- toolArgs: Record<string, unknown>
181
- ): Promise<unknown> {
182
- const result = await this.sendRequest('callTool', { sessionId, toolName, toolArgs });
183
- this.emitUiEventIfPresent(result, sessionId, toolName);
184
- return result;
185
- }
186
-
187
- async restoreSession(sessionId: string): Promise<RestoreSessionResult> {
188
- return this.sendRequest<RestoreSessionResult>('restoreSession', { sessionId });
189
- }
190
-
191
- async finishAuth(sessionId: string, code: string): Promise<FinishAuthResult> {
192
- return this.sendRequest<FinishAuthResult>('finishAuth', { sessionId, code });
193
- }
194
-
195
- async listPrompts(sessionId: string): Promise<ListPromptsResult> {
196
- return this.sendRequest<ListPromptsResult>('listPrompts', { sessionId });
197
- }
198
-
199
- async getPrompt(sessionId: string, name: string, args?: Record<string, string>): Promise<unknown> {
200
- return this.sendRequest('getPrompt', { sessionId, name, args });
201
- }
202
-
203
- async listResources(sessionId: string): Promise<ListResourcesResult> {
204
- return this.sendRequest<ListResourcesResult>('listResources', { sessionId });
205
- }
206
-
207
- async readResource(sessionId: string, uri: string): Promise<unknown> {
208
- return this.sendRequest('readResource', { sessionId, uri });
209
- }
210
-
211
- // ============================================
212
- // Resource Preloading (for instant UI loading)
213
- // ============================================
214
-
215
- /**
216
- * Preload UI resources for tools that have UI metadata.
217
- * Call this when tools are discovered to enable instant MCP App UI loading.
218
- */
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
- }
492
- }
493
- }
1
+ /**
2
+ * Stateless RPC-over-stream client for MCP connections.
3
+ *
4
+ * Uses single POST requests with `Accept: text/event-stream` for every RPC call.
5
+ * Progress events and the final rpc-response are delivered in the same response.
6
+ */
7
+
8
+ import { nanoid } from 'nanoid';
9
+ import type {
10
+ McpConnectionEvent,
11
+ McpObservabilityEvent,
12
+ McpAppsUIEvent
13
+ } from '../../shared/events.js';
14
+ import type {
15
+ McpRpcRequest,
16
+ McpRpcResponse,
17
+ McpRpcMethod,
18
+ McpRpcParams,
19
+ ConnectParams,
20
+ SessionListResult,
21
+ ConnectResult,
22
+ DisconnectResult,
23
+ RestoreSessionResult,
24
+ FinishAuthResult,
25
+ ListToolsRpcResult,
26
+ ListPromptsResult,
27
+ ListResourcesResult,
28
+ } from '../../shared/types.js';
29
+
30
+ export interface SSEClientOptions {
31
+ /** MCP endpoint URL */
32
+ url: string;
33
+
34
+ /** User/Client identifier */
35
+ identity: string;
36
+
37
+ /** Optional auth token for authenticated requests */
38
+ authToken?: string;
39
+
40
+ /** Callback for MCP connection state changes */
41
+ onConnectionEvent?: (event: McpConnectionEvent) => void;
42
+
43
+ /** Callback for observability/logging events */
44
+ onObservabilityEvent?: (event: McpObservabilityEvent) => void;
45
+
46
+ /** Callback for connection status changes */
47
+ onStatusChange?: (status: ConnectionStatus) => void;
48
+
49
+ /** Callback for MCP App UI events */
50
+ onEvent?: (event: McpAppsUIEvent) => void;
51
+
52
+ /** Enable debug logging @default false */
53
+ debug?: boolean;
54
+ }
55
+
56
+ export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
57
+
58
+ interface ToolUiMetadata {
59
+ resourceUri?: string;
60
+ uri?: string;
61
+ visibility?: string[];
62
+ }
63
+
64
+ export class SSEClient {
65
+ private resourceCache = new Map<string, Promise<unknown>>();
66
+ private connected = false;
67
+
68
+ constructor(private readonly options: SSEClientOptions) {}
69
+
70
+ connect(): void {
71
+ if (this.connected) {
72
+ return;
73
+ }
74
+ this.connected = true;
75
+ this.options.onStatusChange?.('connected');
76
+ this.log('RPC mode: post_stream');
77
+ }
78
+
79
+ disconnect(): void {
80
+ this.connected = false;
81
+ this.options.onStatusChange?.('disconnected');
82
+ }
83
+
84
+ isConnected(): boolean {
85
+ return this.connected;
86
+ }
87
+
88
+ async getSessions(): Promise<SessionListResult> {
89
+ return this.sendRequest<SessionListResult>('getSessions');
90
+ }
91
+
92
+ async connectToServer(params: ConnectParams): Promise<ConnectResult> {
93
+ return this.sendRequest<ConnectResult>('connect', params);
94
+ }
95
+
96
+ async disconnectFromServer(sessionId: string): Promise<DisconnectResult> {
97
+ return this.sendRequest<DisconnectResult>('disconnect', { sessionId });
98
+ }
99
+
100
+ async listTools(sessionId: string): Promise<ListToolsRpcResult> {
101
+ return this.sendRequest<ListToolsRpcResult>('listTools', { sessionId });
102
+ }
103
+
104
+ async callTool(
105
+ sessionId: string,
106
+ toolName: string,
107
+ toolArgs: Record<string, unknown>
108
+ ): Promise<unknown> {
109
+ const result = await this.sendRequest('callTool', { sessionId, toolName, toolArgs });
110
+ this.emitUiEventIfPresent(result, sessionId, toolName);
111
+ return result;
112
+ }
113
+
114
+ async restoreSession(sessionId: string): Promise<RestoreSessionResult> {
115
+ return this.sendRequest<RestoreSessionResult>('restoreSession', { sessionId });
116
+ }
117
+
118
+ async finishAuth(sessionId: string, code: string): Promise<FinishAuthResult> {
119
+ return this.sendRequest<FinishAuthResult>('finishAuth', { sessionId, code });
120
+ }
121
+
122
+ async listPrompts(sessionId: string): Promise<ListPromptsResult> {
123
+ return this.sendRequest<ListPromptsResult>('listPrompts', { sessionId });
124
+ }
125
+
126
+ async getPrompt(sessionId: string, name: string, args?: Record<string, string>): Promise<unknown> {
127
+ return this.sendRequest('getPrompt', { sessionId, name, args });
128
+ }
129
+
130
+ async listResources(sessionId: string): Promise<ListResourcesResult> {
131
+ return this.sendRequest<ListResourcesResult>('listResources', { sessionId });
132
+ }
133
+
134
+ async readResource(sessionId: string, uri: string): Promise<unknown> {
135
+ return this.sendRequest('readResource', { sessionId, uri });
136
+ }
137
+
138
+ preloadToolUiResources(sessionId: string, tools: Array<{ name: string; _meta?: unknown }>): void {
139
+ for (const tool of tools) {
140
+ const uri = this.extractUiResourceUri(tool);
141
+ if (!uri || this.resourceCache.has(uri)) continue;
142
+ const promise = this.sendRequest('readResource', { sessionId, uri }).catch((err) => {
143
+ this.log(`Failed to preload resource ${uri}: ${err.message}`, 'warn');
144
+ this.resourceCache.delete(uri);
145
+ return null;
146
+ });
147
+ this.resourceCache.set(uri, promise);
148
+ }
149
+ }
150
+
151
+ getOrFetchResource(sessionId: string, uri: string): Promise<unknown> {
152
+ const cached = this.resourceCache.get(uri);
153
+ if (cached) return cached;
154
+ const promise = this.sendRequest('readResource', { sessionId, uri });
155
+ this.resourceCache.set(uri, promise);
156
+ return promise;
157
+ }
158
+
159
+ hasPreloadedResource(uri: string): boolean {
160
+ return this.resourceCache.has(uri);
161
+ }
162
+
163
+ clearResourceCache(): void {
164
+ this.resourceCache.clear();
165
+ }
166
+
167
+ private async sendRequest<T = unknown>(method: McpRpcMethod, params?: McpRpcParams): Promise<T> {
168
+ if (!this.connected) {
169
+ this.connect();
170
+ }
171
+
172
+ this.log(`RPC request via post_stream: ${method}`);
173
+
174
+ const request: McpRpcRequest = {
175
+ id: `rpc_${nanoid(10)}`,
176
+ method,
177
+ params,
178
+ };
179
+
180
+ const response = await fetch(this.buildUrl(), {
181
+ method: 'POST',
182
+ headers: this.buildHeaders(),
183
+ body: JSON.stringify(request),
184
+ });
185
+
186
+ if (!response.ok) {
187
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
188
+ }
189
+
190
+ const contentType = (response.headers.get('content-type') || '').toLowerCase();
191
+ if (!contentType.includes('text/event-stream')) {
192
+ const data = await response.json() as McpRpcResponse;
193
+ return this.parseRpcResponse<T>(data);
194
+ }
195
+
196
+ const data = await this.readRpcResponseFromStream(response);
197
+ return this.parseRpcResponse<T>(data);
198
+ }
199
+
200
+ private async readRpcResponseFromStream(response: Response): Promise<McpRpcResponse> {
201
+ if (!response.body) {
202
+ throw new Error('Streaming response body is missing');
203
+ }
204
+
205
+ const reader = response.body.getReader();
206
+ const decoder = new TextDecoder();
207
+ let buffer = '';
208
+ let rpcResponse: McpRpcResponse | null = null;
209
+
210
+ const dispatchBlock = (block: string) => {
211
+ const lines = block.split('\n');
212
+ let eventName = 'message';
213
+ const dataLines: string[] = [];
214
+
215
+ for (const rawLine of lines) {
216
+ const line = rawLine.replace(/\r$/, '');
217
+ if (!line || line.startsWith(':')) continue;
218
+ if (line.startsWith('event:')) {
219
+ eventName = line.slice('event:'.length).trim();
220
+ continue;
221
+ }
222
+ if (line.startsWith('data:')) {
223
+ dataLines.push(line.slice('data:'.length).trimStart());
224
+ }
225
+ }
226
+
227
+ if (!dataLines.length) return;
228
+ const payloadText = dataLines.join('\n');
229
+ let payload: unknown = payloadText;
230
+ try {
231
+ payload = JSON.parse(payloadText);
232
+ } catch {
233
+ // Keep raw text
234
+ }
235
+
236
+ switch (eventName) {
237
+ case 'connected':
238
+ this.options.onStatusChange?.('connected');
239
+ break;
240
+ case 'connection':
241
+ this.options.onConnectionEvent?.(payload as McpConnectionEvent);
242
+ break;
243
+ case 'observability':
244
+ this.options.onObservabilityEvent?.(payload as McpObservabilityEvent);
245
+ break;
246
+ case 'rpc-response':
247
+ rpcResponse = payload as McpRpcResponse;
248
+ break;
249
+ default:
250
+ break;
251
+ }
252
+ };
253
+
254
+ while (true) {
255
+ const { value, done } = await reader.read();
256
+ if (done) break;
257
+ buffer += decoder.decode(value, { stream: true });
258
+
259
+ let separatorMatch = buffer.match(/\r?\n\r?\n/);
260
+ while (separatorMatch && separatorMatch.index !== undefined) {
261
+ const separatorIndex = separatorMatch.index;
262
+ const separatorLength = separatorMatch[0].length;
263
+ const block = buffer.slice(0, separatorIndex);
264
+ buffer = buffer.slice(separatorIndex + separatorLength);
265
+ dispatchBlock(block);
266
+ separatorMatch = buffer.match(/\r?\n\r?\n/);
267
+ }
268
+ }
269
+
270
+ if (buffer.trim()) {
271
+ dispatchBlock(buffer);
272
+ }
273
+
274
+ if (!rpcResponse) {
275
+ throw new Error('Missing rpc-response event in streamed RPC result');
276
+ }
277
+
278
+ return rpcResponse;
279
+ }
280
+
281
+ private parseRpcResponse<T>(data: McpRpcResponse): T {
282
+ if ('result' in data) {
283
+ return data.result as T;
284
+ }
285
+ if ('error' in data && data.error) {
286
+ throw new Error(data.error.message || 'Unknown RPC error');
287
+ }
288
+ // JSON omits `result` when it is `undefined` (response becomes `{ id: ... }`).
289
+ // Treat that shape as a successful void result.
290
+ if (data && typeof data === 'object' && 'id' in data) {
291
+ return undefined as T;
292
+ }
293
+ throw new Error('Invalid RPC response format');
294
+ }
295
+
296
+ private buildUrl(): string {
297
+ const url = new URL(this.options.url, globalThis.location?.origin);
298
+ url.searchParams.set('identity', this.options.identity);
299
+ if (this.options.authToken) {
300
+ url.searchParams.set('token', this.options.authToken);
301
+ }
302
+ return url.toString();
303
+ }
304
+
305
+ private buildHeaders(): HeadersInit {
306
+ const headers: HeadersInit = {
307
+ 'Content-Type': 'application/json',
308
+ 'Accept': 'text/event-stream',
309
+ };
310
+ if (this.options.authToken) {
311
+ headers['Authorization'] = `Bearer ${this.options.authToken}`;
312
+ }
313
+ return headers;
314
+ }
315
+
316
+ private extractUiResourceUri(tool: { name: string; _meta?: unknown }): string | undefined {
317
+ const meta = (tool._meta as { ui?: ToolUiMetadata })?.ui;
318
+ if (!meta || typeof meta !== 'object') return undefined;
319
+ if (meta.visibility && !meta.visibility.includes('app')) return undefined;
320
+ return meta.resourceUri ?? meta.uri;
321
+ }
322
+
323
+ private emitUiEventIfPresent(result: unknown, sessionId: string, toolName: string): void {
324
+ const meta = (result as { _meta?: { ui?: ToolUiMetadata } })?._meta;
325
+ const resourceUri = meta?.ui?.resourceUri ?? (meta as any)?.['ui/resourceUri'];
326
+
327
+ if (resourceUri) {
328
+ this.options.onEvent?.({
329
+ type: 'mcp-apps-ui',
330
+ sessionId,
331
+ resourceUri,
332
+ toolName,
333
+ result,
334
+ timestamp: Date.now(),
335
+ });
336
+ }
337
+ }
338
+
339
+ private log(message: string, level: 'info' | 'warn' | 'error' = 'info'): void {
340
+ if (!this.options.debug && level === 'info') return;
341
+
342
+ const prefix = '[SSEClient]';
343
+ switch (level) {
344
+ case 'warn':
345
+ console.warn(prefix, message);
346
+ break;
347
+ case 'error':
348
+ console.error(prefix, message);
349
+ break;
350
+ default:
351
+ console.log(prefix, message);
352
+ }
353
+ }
354
+ }