@mcp-ts/sdk 1.0.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/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/adapters/agui-adapter.d.mts +119 -0
- package/dist/adapters/agui-adapter.d.ts +119 -0
- package/dist/adapters/agui-adapter.js +109 -0
- package/dist/adapters/agui-adapter.js.map +1 -0
- package/dist/adapters/agui-adapter.mjs +107 -0
- package/dist/adapters/agui-adapter.mjs.map +1 -0
- package/dist/adapters/agui-middleware.d.mts +171 -0
- package/dist/adapters/agui-middleware.d.ts +171 -0
- package/dist/adapters/agui-middleware.js +429 -0
- package/dist/adapters/agui-middleware.js.map +1 -0
- package/dist/adapters/agui-middleware.mjs +417 -0
- package/dist/adapters/agui-middleware.mjs.map +1 -0
- package/dist/adapters/ai-adapter.d.mts +38 -0
- package/dist/adapters/ai-adapter.d.ts +38 -0
- package/dist/adapters/ai-adapter.js +82 -0
- package/dist/adapters/ai-adapter.js.map +1 -0
- package/dist/adapters/ai-adapter.mjs +80 -0
- package/dist/adapters/ai-adapter.mjs.map +1 -0
- package/dist/adapters/langchain-adapter.d.mts +46 -0
- package/dist/adapters/langchain-adapter.d.ts +46 -0
- package/dist/adapters/langchain-adapter.js +102 -0
- package/dist/adapters/langchain-adapter.js.map +1 -0
- package/dist/adapters/langchain-adapter.mjs +100 -0
- package/dist/adapters/langchain-adapter.mjs.map +1 -0
- package/dist/adapters/mastra-adapter.d.mts +49 -0
- package/dist/adapters/mastra-adapter.d.ts +49 -0
- package/dist/adapters/mastra-adapter.js +95 -0
- package/dist/adapters/mastra-adapter.js.map +1 -0
- package/dist/adapters/mastra-adapter.mjs +93 -0
- package/dist/adapters/mastra-adapter.mjs.map +1 -0
- package/dist/client/index.d.mts +119 -0
- package/dist/client/index.d.ts +119 -0
- package/dist/client/index.js +225 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/index.mjs +223 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/client/react.d.mts +151 -0
- package/dist/client/react.d.ts +151 -0
- package/dist/client/react.js +492 -0
- package/dist/client/react.js.map +1 -0
- package/dist/client/react.mjs +489 -0
- package/dist/client/react.mjs.map +1 -0
- package/dist/client/vue.d.mts +157 -0
- package/dist/client/vue.d.ts +157 -0
- package/dist/client/vue.js +474 -0
- package/dist/client/vue.js.map +1 -0
- package/dist/client/vue.mjs +471 -0
- package/dist/client/vue.mjs.map +1 -0
- package/dist/events-BP6WyRNh.d.mts +110 -0
- package/dist/events-BP6WyRNh.d.ts +110 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +2784 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2723 -0
- package/dist/index.mjs.map +1 -0
- package/dist/multi-session-client-BOFgPypS.d.ts +389 -0
- package/dist/multi-session-client-DMF3ED2O.d.mts +389 -0
- package/dist/server/index.d.mts +269 -0
- package/dist/server/index.d.ts +269 -0
- package/dist/server/index.js +2444 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +2414 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/shared/index.d.mts +24 -0
- package/dist/shared/index.d.ts +24 -0
- package/dist/shared/index.js +223 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/index.mjs +190 -0
- package/dist/shared/index.mjs.map +1 -0
- package/dist/types-SbDlA2VX.d.mts +153 -0
- package/dist/types-SbDlA2VX.d.ts +153 -0
- package/dist/utils-0qmYrqoa.d.mts +92 -0
- package/dist/utils-0qmYrqoa.d.ts +92 -0
- package/package.json +165 -0
- package/src/adapters/agui-adapter.ts +210 -0
- package/src/adapters/agui-middleware.ts +512 -0
- package/src/adapters/ai-adapter.ts +115 -0
- package/src/adapters/langchain-adapter.ts +127 -0
- package/src/adapters/mastra-adapter.ts +126 -0
- package/src/client/core/sse-client.ts +340 -0
- package/src/client/index.ts +26 -0
- package/src/client/react/index.ts +10 -0
- package/src/client/react/useMcp.ts +558 -0
- package/src/client/vue/index.ts +10 -0
- package/src/client/vue/useMcp.ts +542 -0
- package/src/index.ts +11 -0
- package/src/server/handlers/nextjs-handler.ts +216 -0
- package/src/server/handlers/sse-handler.ts +699 -0
- package/src/server/index.ts +57 -0
- package/src/server/mcp/multi-session-client.ts +132 -0
- package/src/server/mcp/oauth-client.ts +1168 -0
- package/src/server/mcp/storage-oauth-provider.ts +239 -0
- package/src/server/storage/file-backend.ts +169 -0
- package/src/server/storage/index.ts +115 -0
- package/src/server/storage/memory-backend.ts +132 -0
- package/src/server/storage/redis-backend.ts +210 -0
- package/src/server/storage/redis.ts +160 -0
- package/src/server/storage/types.ts +109 -0
- package/src/shared/constants.ts +29 -0
- package/src/shared/errors.ts +133 -0
- package/src/shared/events.ts +166 -0
- package/src/shared/index.ts +70 -0
- package/src/shared/types.ts +274 -0
- package/src/shared/utils.ts +16 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE (Server-Sent Events) Handler for MCP Connections
|
|
3
|
+
* Provides real-time connection state updates to clients
|
|
4
|
+
* Based on Cloudflare's agents pattern but adapted for HTTP/SSE
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { McpConnectionEvent, McpObservabilityEvent } from '../../shared/events.js';
|
|
8
|
+
import type {
|
|
9
|
+
McpRpcRequest,
|
|
10
|
+
McpRpcResponse,
|
|
11
|
+
// RPC Param types
|
|
12
|
+
ConnectParams,
|
|
13
|
+
DisconnectParams,
|
|
14
|
+
SessionParams,
|
|
15
|
+
CallToolParams,
|
|
16
|
+
GetPromptParams,
|
|
17
|
+
ReadResourceParams,
|
|
18
|
+
FinishAuthParams,
|
|
19
|
+
// RPC Result types
|
|
20
|
+
SessionListResult,
|
|
21
|
+
ConnectResult,
|
|
22
|
+
DisconnectResult,
|
|
23
|
+
RestoreSessionResult,
|
|
24
|
+
FinishAuthResult,
|
|
25
|
+
ListToolsRpcResult,
|
|
26
|
+
ListPromptsResult,
|
|
27
|
+
ListResourcesResult,
|
|
28
|
+
} from '../../shared/types.js';
|
|
29
|
+
import { RpcErrorCodes } from '../../shared/errors.js';
|
|
30
|
+
import { MCPClient } from '../mcp/oauth-client.js';
|
|
31
|
+
import { storage } from '../storage/index.js';
|
|
32
|
+
|
|
33
|
+
export interface ClientMetadata {
|
|
34
|
+
clientName?: string;
|
|
35
|
+
clientUri?: string;
|
|
36
|
+
logoUri?: string;
|
|
37
|
+
policyUri?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SSEHandlerOptions {
|
|
41
|
+
/**
|
|
42
|
+
* User/Client identifier
|
|
43
|
+
*/
|
|
44
|
+
identity: string;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Optional callback for authentication/authorization
|
|
48
|
+
*/
|
|
49
|
+
onAuth?: (identity: string) => Promise<boolean>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Heartbeat interval in ms (default: 30000)
|
|
53
|
+
*/
|
|
54
|
+
heartbeatInterval?: number;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Static OAuth client metadata defaults (for all connections)
|
|
58
|
+
*/
|
|
59
|
+
clientDefaults?: ClientMetadata;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Dynamic OAuth client metadata getter (per-request, useful for multi-tenant)
|
|
63
|
+
* Takes precedence over clientDefaults
|
|
64
|
+
*/
|
|
65
|
+
getClientMetadata?: (request?: any) => ClientMetadata | Promise<ClientMetadata>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* SSE Connection Manager
|
|
70
|
+
* Handles a single SSE connection and manages MCP operations
|
|
71
|
+
*/
|
|
72
|
+
export class SSEConnectionManager {
|
|
73
|
+
private identity: string;
|
|
74
|
+
private clients: Map<string, MCPClient> = new Map();
|
|
75
|
+
private heartbeatTimer?: NodeJS.Timeout;
|
|
76
|
+
private isActive: boolean = true;
|
|
77
|
+
|
|
78
|
+
constructor(
|
|
79
|
+
private options: SSEHandlerOptions,
|
|
80
|
+
private sendEvent: (event: McpConnectionEvent | McpObservabilityEvent | McpRpcResponse) => void
|
|
81
|
+
) {
|
|
82
|
+
this.identity = options.identity;
|
|
83
|
+
this.startHeartbeat();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get resolved client metadata (dynamic > static > defaults)
|
|
88
|
+
*/
|
|
89
|
+
private async getResolvedClientMetadata(request?: any): Promise<ClientMetadata> {
|
|
90
|
+
// Priority: getClientMetadata() > clientDefaults > empty object
|
|
91
|
+
let metadata: ClientMetadata = {};
|
|
92
|
+
|
|
93
|
+
// Start with static defaults
|
|
94
|
+
if (this.options.clientDefaults) {
|
|
95
|
+
metadata = { ...this.options.clientDefaults };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Override with dynamic metadata if provided
|
|
99
|
+
if (this.options.getClientMetadata) {
|
|
100
|
+
const dynamicMetadata = await this.options.getClientMetadata(request);
|
|
101
|
+
metadata = { ...metadata, ...dynamicMetadata };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return metadata;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Start heartbeat to keep connection alive
|
|
109
|
+
*/
|
|
110
|
+
private startHeartbeat(): void {
|
|
111
|
+
const interval = this.options.heartbeatInterval || 30000;
|
|
112
|
+
this.heartbeatTimer = setInterval(() => {
|
|
113
|
+
if (this.isActive) {
|
|
114
|
+
this.sendEvent({
|
|
115
|
+
level: 'debug',
|
|
116
|
+
message: 'heartbeat',
|
|
117
|
+
timestamp: Date.now(),
|
|
118
|
+
} as McpObservabilityEvent);
|
|
119
|
+
}
|
|
120
|
+
}, interval);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Handle incoming RPC requests
|
|
125
|
+
*/
|
|
126
|
+
async handleRequest(request: McpRpcRequest): Promise<void> {
|
|
127
|
+
try {
|
|
128
|
+
let result: SessionListResult | ConnectResult | DisconnectResult | RestoreSessionResult | FinishAuthResult | ListToolsRpcResult | ListPromptsResult | ListResourcesResult | unknown;
|
|
129
|
+
|
|
130
|
+
switch (request.method) {
|
|
131
|
+
case 'getSessions':
|
|
132
|
+
result = await this.getSessions();
|
|
133
|
+
break;
|
|
134
|
+
|
|
135
|
+
case 'connect':
|
|
136
|
+
result = await this.connect(request.params as ConnectParams);
|
|
137
|
+
break;
|
|
138
|
+
|
|
139
|
+
case 'disconnect':
|
|
140
|
+
result = await this.disconnect(request.params as DisconnectParams);
|
|
141
|
+
break;
|
|
142
|
+
|
|
143
|
+
case 'listTools':
|
|
144
|
+
result = await this.listTools(request.params as SessionParams);
|
|
145
|
+
break;
|
|
146
|
+
|
|
147
|
+
case 'callTool':
|
|
148
|
+
result = await this.callTool(request.params as CallToolParams);
|
|
149
|
+
break;
|
|
150
|
+
|
|
151
|
+
case 'restoreSession':
|
|
152
|
+
result = await this.restoreSession(request.params as SessionParams);
|
|
153
|
+
break;
|
|
154
|
+
|
|
155
|
+
case 'finishAuth':
|
|
156
|
+
result = await this.finishAuth(request.params as FinishAuthParams);
|
|
157
|
+
break;
|
|
158
|
+
|
|
159
|
+
case 'listPrompts':
|
|
160
|
+
result = await this.listPrompts(request.params as SessionParams);
|
|
161
|
+
break;
|
|
162
|
+
|
|
163
|
+
case 'getPrompt':
|
|
164
|
+
result = await this.getPrompt(request.params as GetPromptParams);
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
case 'listResources':
|
|
168
|
+
result = await this.listResources(request.params as SessionParams);
|
|
169
|
+
break;
|
|
170
|
+
|
|
171
|
+
case 'readResource':
|
|
172
|
+
result = await this.readResource(request.params as ReadResourceParams);
|
|
173
|
+
break;
|
|
174
|
+
|
|
175
|
+
default:
|
|
176
|
+
throw new Error(`Unknown method: ${request.method}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.sendEvent({
|
|
180
|
+
id: request.id,
|
|
181
|
+
result,
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
this.sendEvent({
|
|
185
|
+
id: request.id,
|
|
186
|
+
error: {
|
|
187
|
+
code: RpcErrorCodes.EXECUTION_ERROR,
|
|
188
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get all user sessions
|
|
196
|
+
*/
|
|
197
|
+
private async getSessions(): Promise<SessionListResult> {
|
|
198
|
+
const sessions = await storage.getIdentitySessionsData(this.identity);
|
|
199
|
+
|
|
200
|
+
this.sendEvent({
|
|
201
|
+
level: 'debug',
|
|
202
|
+
message: `Retrieved ${sessions.length} sessions for identity ${this.identity}`,
|
|
203
|
+
timestamp: Date.now(),
|
|
204
|
+
metadata: {
|
|
205
|
+
identity: this.identity,
|
|
206
|
+
sessionCount: sessions.length,
|
|
207
|
+
sessions: sessions.map(s => ({
|
|
208
|
+
sessionId: s.sessionId,
|
|
209
|
+
serverId: s.serverId,
|
|
210
|
+
serverName: s.serverName,
|
|
211
|
+
})),
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
sessions: sessions.map((s) => ({
|
|
217
|
+
sessionId: s.sessionId,
|
|
218
|
+
serverId: s.serverId,
|
|
219
|
+
serverName: s.serverName,
|
|
220
|
+
serverUrl: s.serverUrl,
|
|
221
|
+
transport: s.transportType,
|
|
222
|
+
})),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Connect to an MCP server
|
|
228
|
+
*/
|
|
229
|
+
private async connect(params: ConnectParams): Promise<ConnectResult> {
|
|
230
|
+
const { serverId, serverName, serverUrl, callbackUrl, transportType } = params;
|
|
231
|
+
|
|
232
|
+
// Check for existing connections
|
|
233
|
+
const existingSessions = await storage.getIdentitySessionsData(this.identity);
|
|
234
|
+
const duplicate = existingSessions.find(s =>
|
|
235
|
+
s.serverId === serverId || s.serverUrl === serverUrl
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
if (duplicate) {
|
|
239
|
+
throw new Error(`Connection already exists for server: ${duplicate.serverUrl || duplicate.serverId} (${duplicate.serverName})`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Generate session ID
|
|
243
|
+
const sessionId = await storage.generateSessionId();
|
|
244
|
+
|
|
245
|
+
// Emit connecting state
|
|
246
|
+
this.emitConnectionEvent({
|
|
247
|
+
type: 'state_changed',
|
|
248
|
+
sessionId,
|
|
249
|
+
serverId,
|
|
250
|
+
serverName,
|
|
251
|
+
state: 'CONNECTING',
|
|
252
|
+
previousState: 'DISCONNECTED',
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
// Get resolved client metadata
|
|
258
|
+
const clientMetadata = await this.getResolvedClientMetadata();
|
|
259
|
+
|
|
260
|
+
// Create MCP client
|
|
261
|
+
const client = new MCPClient({
|
|
262
|
+
identity: this.identity,
|
|
263
|
+
sessionId,
|
|
264
|
+
serverId,
|
|
265
|
+
serverName,
|
|
266
|
+
serverUrl,
|
|
267
|
+
callbackUrl,
|
|
268
|
+
transportType,
|
|
269
|
+
...clientMetadata, // Spread client metadata (clientName, clientUri, logoUri, policyUri)
|
|
270
|
+
onRedirect: (authUrl) => {
|
|
271
|
+
// Emit auth required event
|
|
272
|
+
this.emitConnectionEvent({
|
|
273
|
+
type: 'auth_required',
|
|
274
|
+
sessionId,
|
|
275
|
+
serverId,
|
|
276
|
+
authUrl,
|
|
277
|
+
timestamp: Date.now(),
|
|
278
|
+
});
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Note: Session will be created by MCPClient after successful connection
|
|
283
|
+
// This ensures sessions only exist for successful or OAuth-pending connections
|
|
284
|
+
|
|
285
|
+
// Store client
|
|
286
|
+
this.clients.set(sessionId, client);
|
|
287
|
+
|
|
288
|
+
// Subscribe to client events
|
|
289
|
+
client.onConnectionEvent((event) => {
|
|
290
|
+
this.emitConnectionEvent(event);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
client.onObservabilityEvent((event) => {
|
|
294
|
+
this.sendEvent(event);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Attempt connection
|
|
298
|
+
await client.connect();
|
|
299
|
+
|
|
300
|
+
// Fetch tools
|
|
301
|
+
const tools = await client.listTools();
|
|
302
|
+
|
|
303
|
+
// Debug: Check session state after connection
|
|
304
|
+
const sessionAfterConnect = await storage.getSession(this.identity, sessionId);
|
|
305
|
+
console.log(`[SSE Handler] After connect() - Session ${sessionId}:`, {
|
|
306
|
+
serverId: sessionAfterConnect?.serverId,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
this.emitConnectionEvent({
|
|
310
|
+
type: 'tools_discovered',
|
|
311
|
+
sessionId,
|
|
312
|
+
serverId,
|
|
313
|
+
toolCount: tools.tools.length,
|
|
314
|
+
tools: tools.tools,
|
|
315
|
+
timestamp: Date.now(),
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
sessionId,
|
|
320
|
+
success: true,
|
|
321
|
+
};
|
|
322
|
+
} catch (error) {
|
|
323
|
+
this.emitConnectionEvent({
|
|
324
|
+
type: 'error',
|
|
325
|
+
sessionId,
|
|
326
|
+
serverId,
|
|
327
|
+
error: error instanceof Error ? error.message : 'Connection failed',
|
|
328
|
+
errorType: 'connection',
|
|
329
|
+
timestamp: Date.now(),
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Clean up client
|
|
333
|
+
this.clients.delete(sessionId);
|
|
334
|
+
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Disconnect from an MCP server
|
|
341
|
+
*/
|
|
342
|
+
private async disconnect(params: DisconnectParams): Promise<DisconnectResult> {
|
|
343
|
+
const { sessionId } = params;
|
|
344
|
+
const client = this.clients.get(sessionId);
|
|
345
|
+
|
|
346
|
+
if (client) {
|
|
347
|
+
await client.clearSession();
|
|
348
|
+
client.disconnect();
|
|
349
|
+
this.clients.delete(sessionId);
|
|
350
|
+
} else {
|
|
351
|
+
// Handle orphaned sessions (e.g., OAuth flow failed before client was stored)
|
|
352
|
+
// Directly remove from storage since there's no active client
|
|
353
|
+
await storage.removeSession(this.identity, sessionId);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return { success: true };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Helper to get or restore a client
|
|
361
|
+
*/
|
|
362
|
+
private async getOrCreateClient(sessionId: string): Promise<MCPClient> {
|
|
363
|
+
let client = this.clients.get(sessionId);
|
|
364
|
+
|
|
365
|
+
if (!client) {
|
|
366
|
+
client = new MCPClient({
|
|
367
|
+
identity: this.identity,
|
|
368
|
+
sessionId,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Subscribe to events
|
|
372
|
+
client.onConnectionEvent((event) => {
|
|
373
|
+
this.emitConnectionEvent(event);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
client.onObservabilityEvent((event) => {
|
|
377
|
+
this.sendEvent(event);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
await client.connect();
|
|
381
|
+
this.clients.set(sessionId, client);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return client;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* List tools from a session
|
|
389
|
+
*/
|
|
390
|
+
private async listTools(params: SessionParams): Promise<ListToolsRpcResult> {
|
|
391
|
+
const { sessionId } = params;
|
|
392
|
+
const client = await this.getOrCreateClient(sessionId);
|
|
393
|
+
const result = await client.listTools();
|
|
394
|
+
return { tools: result.tools };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Call a tool
|
|
399
|
+
*/
|
|
400
|
+
private async callTool(params: CallToolParams): Promise<unknown> {
|
|
401
|
+
const { sessionId, toolName, toolArgs } = params;
|
|
402
|
+
const client = await this.getOrCreateClient(sessionId);
|
|
403
|
+
return await client.callTool(toolName, toolArgs);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Refresh/validate a session
|
|
408
|
+
*/
|
|
409
|
+
private async restoreSession(params: SessionParams): Promise<RestoreSessionResult> {
|
|
410
|
+
const { sessionId } = params;
|
|
411
|
+
|
|
412
|
+
this.sendEvent({
|
|
413
|
+
level: 'debug',
|
|
414
|
+
message: `Starting session refresh for ${sessionId}`,
|
|
415
|
+
timestamp: Date.now(),
|
|
416
|
+
metadata: { sessionId, identity: this.identity },
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Emit validating state
|
|
420
|
+
const session = await storage.getSession(this.identity, sessionId);
|
|
421
|
+
if (!session) {
|
|
422
|
+
this.sendEvent({
|
|
423
|
+
level: 'error',
|
|
424
|
+
message: `Session not found: ${sessionId}`,
|
|
425
|
+
timestamp: Date.now(),
|
|
426
|
+
metadata: { sessionId, identity: this.identity },
|
|
427
|
+
});
|
|
428
|
+
throw new Error('Session not found');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
this.sendEvent({
|
|
432
|
+
level: 'debug',
|
|
433
|
+
message: `Session found in Redis`,
|
|
434
|
+
timestamp: Date.now(),
|
|
435
|
+
metadata: {
|
|
436
|
+
sessionId,
|
|
437
|
+
serverId: session.serverId,
|
|
438
|
+
serverName: session.serverName,
|
|
439
|
+
serverUrl: session.serverUrl,
|
|
440
|
+
transportType: session.transportType,
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
this.emitConnectionEvent({
|
|
445
|
+
type: 'state_changed',
|
|
446
|
+
sessionId,
|
|
447
|
+
serverId: session.serverId || 'unknown',
|
|
448
|
+
serverName: session.serverName || 'Unknown',
|
|
449
|
+
state: 'VALIDATING',
|
|
450
|
+
previousState: 'DISCONNECTED',
|
|
451
|
+
timestamp: Date.now(),
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
// Get resolved client metadata
|
|
456
|
+
const clientMetadata = await this.getResolvedClientMetadata();
|
|
457
|
+
|
|
458
|
+
// Try to restore and validate
|
|
459
|
+
const client = new MCPClient({
|
|
460
|
+
identity: this.identity,
|
|
461
|
+
sessionId,
|
|
462
|
+
...clientMetadata, // Include metadata for consistency
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// Subscribe to events
|
|
466
|
+
client.onConnectionEvent((event) => {
|
|
467
|
+
this.emitConnectionEvent(event);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
client.onObservabilityEvent((event) => {
|
|
471
|
+
this.sendEvent(event);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
await client.connect();
|
|
475
|
+
this.clients.set(sessionId, client);
|
|
476
|
+
|
|
477
|
+
const tools = await client.listTools();
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
this.emitConnectionEvent({
|
|
482
|
+
type: 'tools_discovered',
|
|
483
|
+
sessionId,
|
|
484
|
+
serverId: session.serverId || 'unknown',
|
|
485
|
+
toolCount: tools.tools.length,
|
|
486
|
+
tools: tools.tools,
|
|
487
|
+
timestamp: Date.now(),
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
return { success: true, toolCount: tools.tools.length };
|
|
491
|
+
} catch (error) {
|
|
492
|
+
this.emitConnectionEvent({
|
|
493
|
+
type: 'error',
|
|
494
|
+
sessionId,
|
|
495
|
+
serverId: session.serverId || 'unknown',
|
|
496
|
+
error: error instanceof Error ? error.message : 'Validation failed',
|
|
497
|
+
errorType: 'validation',
|
|
498
|
+
timestamp: Date.now(),
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Complete OAuth authorization
|
|
507
|
+
*/
|
|
508
|
+
private async finishAuth(params: FinishAuthParams): Promise<FinishAuthResult> {
|
|
509
|
+
const { sessionId, code } = params;
|
|
510
|
+
|
|
511
|
+
this.sendEvent({
|
|
512
|
+
level: 'debug',
|
|
513
|
+
message: `Completing OAuth for session ${sessionId}`,
|
|
514
|
+
timestamp: Date.now(),
|
|
515
|
+
metadata: { sessionId, identity: this.identity },
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
const session = await storage.getSession(this.identity, sessionId);
|
|
519
|
+
if (!session) {
|
|
520
|
+
throw new Error('Session not found');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
this.emitConnectionEvent({
|
|
524
|
+
type: 'state_changed',
|
|
525
|
+
sessionId,
|
|
526
|
+
serverId: session.serverId || 'unknown',
|
|
527
|
+
serverName: session.serverName || 'Unknown',
|
|
528
|
+
state: 'AUTHENTICATING',
|
|
529
|
+
previousState: 'DISCONNECTED',
|
|
530
|
+
timestamp: Date.now(),
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
const client = new MCPClient({
|
|
535
|
+
identity: this.identity,
|
|
536
|
+
sessionId,
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// Subscribe to events
|
|
540
|
+
client.onConnectionEvent((event) => {
|
|
541
|
+
this.emitConnectionEvent(event);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
await client.finishAuth(code);
|
|
545
|
+
this.clients.set(sessionId, client);
|
|
546
|
+
|
|
547
|
+
const tools = await client.listTools();
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
this.emitConnectionEvent({
|
|
552
|
+
type: 'tools_discovered',
|
|
553
|
+
sessionId,
|
|
554
|
+
serverId: session.serverId || 'unknown',
|
|
555
|
+
toolCount: tools.tools.length,
|
|
556
|
+
tools: tools.tools,
|
|
557
|
+
timestamp: Date.now(),
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
return { success: true, toolCount: tools.tools.length };
|
|
561
|
+
} catch (error) {
|
|
562
|
+
this.emitConnectionEvent({
|
|
563
|
+
type: 'error',
|
|
564
|
+
sessionId,
|
|
565
|
+
serverId: session.serverId || 'unknown',
|
|
566
|
+
error: error instanceof Error ? error.message : 'OAuth completion failed',
|
|
567
|
+
errorType: 'auth',
|
|
568
|
+
timestamp: Date.now(),
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
throw error;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* List prompts from a session
|
|
577
|
+
*/
|
|
578
|
+
private async listPrompts(params: SessionParams): Promise<ListPromptsResult> {
|
|
579
|
+
const { sessionId } = params;
|
|
580
|
+
const client = await this.getOrCreateClient(sessionId);
|
|
581
|
+
const result = await client.listPrompts();
|
|
582
|
+
return { prompts: result.prompts };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Get a specific prompt
|
|
587
|
+
*/
|
|
588
|
+
private async getPrompt(params: GetPromptParams): Promise<unknown> {
|
|
589
|
+
const { sessionId, name, args } = params;
|
|
590
|
+
const client = await this.getOrCreateClient(sessionId);
|
|
591
|
+
return await client.getPrompt(name, args);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* List resources from a session
|
|
596
|
+
*/
|
|
597
|
+
private async listResources(params: SessionParams): Promise<ListResourcesResult> {
|
|
598
|
+
const { sessionId } = params;
|
|
599
|
+
const client = await this.getOrCreateClient(sessionId);
|
|
600
|
+
const result = await client.listResources();
|
|
601
|
+
return { resources: result.resources };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Read a specific resource
|
|
606
|
+
*/
|
|
607
|
+
private async readResource(params: ReadResourceParams): Promise<unknown> {
|
|
608
|
+
const { sessionId, uri } = params;
|
|
609
|
+
const client = await this.getOrCreateClient(sessionId);
|
|
610
|
+
return await client.readResource(uri);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Emit connection event
|
|
615
|
+
*/
|
|
616
|
+
private emitConnectionEvent(event: McpConnectionEvent): void {
|
|
617
|
+
this.sendEvent(event);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Cleanup and close all connections
|
|
622
|
+
*/
|
|
623
|
+
dispose(): void {
|
|
624
|
+
this.isActive = false;
|
|
625
|
+
|
|
626
|
+
if (this.heartbeatTimer) {
|
|
627
|
+
clearInterval(this.heartbeatTimer);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
for (const client of this.clients.values()) {
|
|
631
|
+
client.disconnect();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
this.clients.clear();
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Create SSE endpoint handler
|
|
640
|
+
* Compatible with various Node.js frameworks
|
|
641
|
+
*/
|
|
642
|
+
export function createSSEHandler(options: SSEHandlerOptions) {
|
|
643
|
+
return async (req: any, res: any) => {
|
|
644
|
+
// Set SSE headers
|
|
645
|
+
res.writeHead(200, {
|
|
646
|
+
'Content-Type': 'text/event-stream',
|
|
647
|
+
'Cache-Control': 'no-cache',
|
|
648
|
+
'Connection': 'keep-alive',
|
|
649
|
+
'Access-Control-Allow-Origin': '*',
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// Send initial connection event
|
|
653
|
+
sendSSE(res, 'connected', { timestamp: Date.now() });
|
|
654
|
+
|
|
655
|
+
// Create connection manager
|
|
656
|
+
const manager = new SSEConnectionManager(options, (event) => {
|
|
657
|
+
// Determine event type
|
|
658
|
+
if ('id' in event) {
|
|
659
|
+
// RPC response
|
|
660
|
+
sendSSE(res, 'rpc-response', event);
|
|
661
|
+
} else if ('type' in event && 'sessionId' in event) {
|
|
662
|
+
// Connection event
|
|
663
|
+
sendSSE(res, 'connection', event);
|
|
664
|
+
} else {
|
|
665
|
+
// Observability event
|
|
666
|
+
sendSSE(res, 'observability', event);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// Handle client disconnect
|
|
671
|
+
req.on('close', () => {
|
|
672
|
+
manager.dispose();
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// Handle incoming messages (if using POST body or other methods)
|
|
676
|
+
if (req.method === 'POST') {
|
|
677
|
+
let body = '';
|
|
678
|
+
req.on('data', (chunk: Buffer) => {
|
|
679
|
+
body += chunk.toString();
|
|
680
|
+
});
|
|
681
|
+
req.on('end', async () => {
|
|
682
|
+
try {
|
|
683
|
+
const request: McpRpcRequest = JSON.parse(body);
|
|
684
|
+
await manager.handleRequest(request);
|
|
685
|
+
} catch (error) {
|
|
686
|
+
console.error('[SSE] Error handling request:', error);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Send SSE event
|
|
695
|
+
*/
|
|
696
|
+
function sendSSE(res: any, event: string, data: any): void {
|
|
697
|
+
res.write(`event: ${event}\n`);
|
|
698
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
699
|
+
}
|