@mcp-ts/sdk 1.3.7 → 1.3.10

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