@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.
- package/README.md +25 -13
- package/dist/adapters/agui-adapter.d.mts +21 -44
- package/dist/adapters/agui-adapter.d.ts +21 -44
- package/dist/adapters/agui-adapter.js +93 -67
- package/dist/adapters/agui-adapter.js.map +1 -1
- package/dist/adapters/agui-adapter.mjs +93 -68
- package/dist/adapters/agui-adapter.mjs.map +1 -1
- package/dist/adapters/agui-middleware.d.mts +32 -134
- package/dist/adapters/agui-middleware.d.ts +32 -134
- package/dist/adapters/agui-middleware.js +314 -350
- package/dist/adapters/agui-middleware.js.map +1 -1
- package/dist/adapters/agui-middleware.mjs +314 -351
- package/dist/adapters/agui-middleware.mjs.map +1 -1
- package/dist/adapters/ai-adapter.d.mts +2 -2
- package/dist/adapters/ai-adapter.d.ts +2 -2
- package/dist/adapters/langchain-adapter.d.mts +2 -2
- package/dist/adapters/langchain-adapter.d.ts +2 -2
- package/dist/adapters/mastra-adapter.d.mts +2 -2
- package/dist/adapters/mastra-adapter.d.ts +2 -2
- package/dist/client/index.d.mts +184 -57
- package/dist/client/index.d.ts +184 -57
- package/dist/client/index.js +535 -130
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +535 -131
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +40 -6
- package/dist/client/react.d.ts +40 -6
- package/dist/client/react.js +587 -142
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +586 -143
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +5 -5
- package/dist/client/vue.d.ts +5 -5
- package/dist/client/vue.js +545 -140
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +545 -141
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{events-BP6WyRNh.d.mts → events-BgeztGYZ.d.mts} +12 -1
- package/dist/{events-BP6WyRNh.d.ts → events-BgeztGYZ.d.ts} +12 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +779 -248
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +775 -245
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-DMF3ED2O.d.mts → multi-session-client-CxogNckF.d.mts} +1 -1
- package/dist/{multi-session-client-BOFgPypS.d.ts → multi-session-client-cox_WXUj.d.ts} +1 -1
- package/dist/server/index.d.mts +44 -40
- package/dist/server/index.d.ts +44 -40
- package/dist/server/index.js +242 -116
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +238 -112
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +2 -2
- package/dist/shared/index.d.ts +2 -2
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{types-SbDlA2VX.d.mts → types-CLccx9wW.d.mts} +1 -1
- package/dist/{types-SbDlA2VX.d.ts → types-CLccx9wW.d.ts} +1 -1
- package/package.json +8 -1
- package/src/adapters/agui-adapter.ts +121 -107
- package/src/adapters/agui-middleware.ts +474 -512
- package/src/client/core/app-host.ts +417 -0
- package/src/client/core/sse-client.ts +365 -212
- package/src/client/core/types.ts +31 -0
- package/src/client/index.ts +1 -0
- package/src/client/react/index.ts +1 -0
- package/src/client/react/use-mcp-app.ts +73 -0
- package/src/client/react/useMcp.ts +18 -0
- package/src/server/handlers/nextjs-handler.ts +8 -7
- package/src/server/handlers/sse-handler.ts +131 -164
- package/src/server/mcp/oauth-client.ts +32 -2
- package/src/server/storage/index.ts +17 -1
- package/src/server/storage/sqlite-backend.ts +185 -0
- package/src/server/storage/types.ts +1 -1
- package/src/shared/events.ts +12 -0
- package/src/shared/types.ts +4 -2
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SSE Client for MCP Connections
|
|
3
|
-
*
|
|
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 {
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
private
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
338
|
-
|
|
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
|
}
|