@claude-flow/mcp 3.0.0-alpha.1
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/.agentic-flow/intelligence.json +16 -0
- package/README.md +428 -0
- package/__tests__/integration.test.ts +449 -0
- package/__tests__/mcp.test.ts +641 -0
- package/dist/connection-pool.d.ts +36 -0
- package/dist/connection-pool.d.ts.map +1 -0
- package/dist/connection-pool.js +273 -0
- package/dist/connection-pool.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +85 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth.d.ts +146 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +318 -0
- package/dist/oauth.js.map +1 -0
- package/dist/prompt-registry.d.ts +90 -0
- package/dist/prompt-registry.d.ts.map +1 -0
- package/dist/prompt-registry.js +209 -0
- package/dist/prompt-registry.js.map +1 -0
- package/dist/rate-limiter.d.ts +86 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +197 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/resource-registry.d.ts +144 -0
- package/dist/resource-registry.d.ts.map +1 -0
- package/dist/resource-registry.js +405 -0
- package/dist/resource-registry.js.map +1 -0
- package/dist/sampling.d.ts +102 -0
- package/dist/sampling.d.ts.map +1 -0
- package/dist/sampling.js +268 -0
- package/dist/sampling.js.map +1 -0
- package/dist/schema-validator.d.ts +30 -0
- package/dist/schema-validator.d.ts.map +1 -0
- package/dist/schema-validator.js +182 -0
- package/dist/schema-validator.js.map +1 -0
- package/dist/server.d.ts +122 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +829 -0
- package/dist/server.js.map +1 -0
- package/dist/session-manager.d.ts +55 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +252 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/task-manager.d.ts +81 -0
- package/dist/task-manager.d.ts.map +1 -0
- package/dist/task-manager.js +337 -0
- package/dist/task-manager.js.map +1 -0
- package/dist/tool-registry.d.ts +88 -0
- package/dist/tool-registry.d.ts.map +1 -0
- package/dist/tool-registry.js +353 -0
- package/dist/tool-registry.js.map +1 -0
- package/dist/transport/http.d.ts +55 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +446 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/index.d.ts +50 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +181 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/stdio.d.ts +43 -0
- package/dist/transport/stdio.d.ts.map +1 -0
- package/dist/transport/stdio.js +194 -0
- package/dist/transport/stdio.js.map +1 -0
- package/dist/transport/websocket.d.ts +65 -0
- package/dist/transport/websocket.d.ts.map +1 -0
- package/dist/transport/websocket.js +314 -0
- package/dist/transport/websocket.js.map +1 -0
- package/dist/types.d.ts +473 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +40 -0
- package/dist/types.js.map +1 -0
- package/package.json +42 -0
- package/src/connection-pool.ts +344 -0
- package/src/index.ts +253 -0
- package/src/oauth.ts +447 -0
- package/src/prompt-registry.ts +296 -0
- package/src/rate-limiter.ts +266 -0
- package/src/resource-registry.ts +530 -0
- package/src/sampling.ts +363 -0
- package/src/schema-validator.ts +213 -0
- package/src/server.ts +1134 -0
- package/src/session-manager.ts +339 -0
- package/src/task-manager.ts +427 -0
- package/src/tool-registry.ts +475 -0
- package/src/transport/http.ts +532 -0
- package/src/transport/index.ts +233 -0
- package/src/transport/stdio.ts +252 -0
- package/src/transport/websocket.ts +396 -0
- package/src/types.ts +664 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @claude-flow/mcp - WebSocket Transport
|
|
3
|
+
*
|
|
4
|
+
* Standalone WebSocket transport with heartbeat
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
import { WebSocketServer, WebSocket, RawData } from 'ws';
|
|
9
|
+
import { createServer, Server } from 'http';
|
|
10
|
+
import type {
|
|
11
|
+
ITransport,
|
|
12
|
+
TransportType,
|
|
13
|
+
MCPRequest,
|
|
14
|
+
MCPResponse,
|
|
15
|
+
MCPNotification,
|
|
16
|
+
RequestHandler,
|
|
17
|
+
NotificationHandler,
|
|
18
|
+
TransportHealthStatus,
|
|
19
|
+
ILogger,
|
|
20
|
+
AuthConfig,
|
|
21
|
+
} from '../types.js';
|
|
22
|
+
|
|
23
|
+
export interface WebSocketTransportConfig {
|
|
24
|
+
host: string;
|
|
25
|
+
port: number;
|
|
26
|
+
path?: string;
|
|
27
|
+
maxConnections?: number;
|
|
28
|
+
heartbeatInterval?: number;
|
|
29
|
+
heartbeatTimeout?: number;
|
|
30
|
+
maxMessageSize?: number;
|
|
31
|
+
auth?: AuthConfig;
|
|
32
|
+
enableBinaryMode?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ClientConnection {
|
|
36
|
+
id: string;
|
|
37
|
+
ws: WebSocket;
|
|
38
|
+
createdAt: Date;
|
|
39
|
+
lastActivity: Date;
|
|
40
|
+
messageCount: number;
|
|
41
|
+
isAlive: boolean;
|
|
42
|
+
isAuthenticated: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class WebSocketTransport extends EventEmitter implements ITransport {
|
|
46
|
+
public readonly type: TransportType = 'websocket';
|
|
47
|
+
|
|
48
|
+
private requestHandler?: RequestHandler;
|
|
49
|
+
private notificationHandler?: NotificationHandler;
|
|
50
|
+
private server?: Server;
|
|
51
|
+
private wss?: WebSocketServer;
|
|
52
|
+
private clients: Map<string, ClientConnection> = new Map();
|
|
53
|
+
private heartbeatTimer?: NodeJS.Timeout;
|
|
54
|
+
private running = false;
|
|
55
|
+
private connectionCounter = 0;
|
|
56
|
+
|
|
57
|
+
private messagesReceived = 0;
|
|
58
|
+
private messagesSent = 0;
|
|
59
|
+
private errors = 0;
|
|
60
|
+
private totalConnections = 0;
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
private readonly logger: ILogger,
|
|
64
|
+
private readonly config: WebSocketTransportConfig
|
|
65
|
+
) {
|
|
66
|
+
super();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async start(): Promise<void> {
|
|
70
|
+
if (this.running) {
|
|
71
|
+
throw new Error('WebSocket transport already running');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.logger.info('Starting WebSocket transport', {
|
|
75
|
+
host: this.config.host,
|
|
76
|
+
port: this.config.port,
|
|
77
|
+
path: this.config.path || '/ws',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this.server = createServer((req, res) => {
|
|
81
|
+
res.writeHead(426, { 'Content-Type': 'text/plain' });
|
|
82
|
+
res.end('Upgrade Required - WebSocket connection expected');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
this.wss = new WebSocketServer({
|
|
86
|
+
server: this.server,
|
|
87
|
+
path: this.config.path || '/ws',
|
|
88
|
+
maxPayload: this.config.maxMessageSize || 10 * 1024 * 1024,
|
|
89
|
+
perMessageDeflate: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.setupWebSocketHandlers();
|
|
93
|
+
this.startHeartbeat();
|
|
94
|
+
|
|
95
|
+
await new Promise<void>((resolve, reject) => {
|
|
96
|
+
this.server!.listen(this.config.port, this.config.host, () => {
|
|
97
|
+
resolve();
|
|
98
|
+
});
|
|
99
|
+
this.server!.on('error', reject);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
this.running = true;
|
|
103
|
+
this.logger.info('WebSocket transport started', {
|
|
104
|
+
url: `ws://${this.config.host}:${this.config.port}${this.config.path || '/ws'}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async stop(): Promise<void> {
|
|
109
|
+
if (!this.running) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.logger.info('Stopping WebSocket transport');
|
|
114
|
+
this.running = false;
|
|
115
|
+
|
|
116
|
+
this.stopHeartbeat();
|
|
117
|
+
|
|
118
|
+
for (const client of this.clients.values()) {
|
|
119
|
+
try {
|
|
120
|
+
client.ws.close(1000, 'Server shutting down');
|
|
121
|
+
} catch {
|
|
122
|
+
// Ignore errors
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
this.clients.clear();
|
|
126
|
+
|
|
127
|
+
if (this.wss) {
|
|
128
|
+
this.wss.close();
|
|
129
|
+
this.wss = undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (this.server) {
|
|
133
|
+
await new Promise<void>((resolve) => {
|
|
134
|
+
this.server!.close(() => resolve());
|
|
135
|
+
});
|
|
136
|
+
this.server = undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.logger.info('WebSocket transport stopped');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
onRequest(handler: RequestHandler): void {
|
|
143
|
+
this.requestHandler = handler;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
onNotification(handler: NotificationHandler): void {
|
|
147
|
+
this.notificationHandler = handler;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async getHealthStatus(): Promise<TransportHealthStatus> {
|
|
151
|
+
return {
|
|
152
|
+
healthy: this.running,
|
|
153
|
+
metrics: {
|
|
154
|
+
messagesReceived: this.messagesReceived,
|
|
155
|
+
messagesSent: this.messagesSent,
|
|
156
|
+
errors: this.errors,
|
|
157
|
+
activeConnections: this.clients.size,
|
|
158
|
+
totalConnections: this.totalConnections,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async sendNotification(notification: MCPNotification): Promise<void> {
|
|
164
|
+
const message = this.serializeMessage(notification);
|
|
165
|
+
|
|
166
|
+
for (const client of this.clients.values()) {
|
|
167
|
+
try {
|
|
168
|
+
if (client.ws.readyState === WebSocket.OPEN) {
|
|
169
|
+
client.ws.send(message);
|
|
170
|
+
this.messagesSent++;
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
this.logger.error('Failed to send notification', { clientId: client.id, error });
|
|
174
|
+
this.errors++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async sendToClient(clientId: string, notification: MCPNotification): Promise<boolean> {
|
|
180
|
+
const client = this.clients.get(clientId);
|
|
181
|
+
if (!client || client.ws.readyState !== WebSocket.OPEN) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
client.ws.send(this.serializeMessage(notification));
|
|
187
|
+
this.messagesSent++;
|
|
188
|
+
return true;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
this.logger.error('Failed to send to client', { clientId, error });
|
|
191
|
+
this.errors++;
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
getClients(): string[] {
|
|
197
|
+
return Array.from(this.clients.keys());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
getClientInfo(clientId: string): ClientConnection | undefined {
|
|
201
|
+
return this.clients.get(clientId);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
disconnectClient(clientId: string, reason = 'Disconnected by server'): boolean {
|
|
205
|
+
const client = this.clients.get(clientId);
|
|
206
|
+
if (!client) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
client.ws.close(1000, reason);
|
|
212
|
+
return true;
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private setupWebSocketHandlers(): void {
|
|
219
|
+
if (!this.wss) return;
|
|
220
|
+
|
|
221
|
+
this.wss.on('connection', (ws) => {
|
|
222
|
+
if (this.config.maxConnections && this.clients.size >= this.config.maxConnections) {
|
|
223
|
+
this.logger.warn('Max connections reached, rejecting client');
|
|
224
|
+
ws.close(1013, 'Server at capacity');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const clientId = `client-${++this.connectionCounter}`;
|
|
229
|
+
const client: ClientConnection = {
|
|
230
|
+
id: clientId,
|
|
231
|
+
ws,
|
|
232
|
+
createdAt: new Date(),
|
|
233
|
+
lastActivity: new Date(),
|
|
234
|
+
messageCount: 0,
|
|
235
|
+
isAlive: true,
|
|
236
|
+
isAuthenticated: !this.config.auth?.enabled,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
this.clients.set(clientId, client);
|
|
240
|
+
this.totalConnections++;
|
|
241
|
+
|
|
242
|
+
this.logger.info('Client connected', {
|
|
243
|
+
id: clientId,
|
|
244
|
+
total: this.clients.size,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
ws.on('message', async (data) => {
|
|
248
|
+
await this.handleMessage(client, data);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
ws.on('pong', () => {
|
|
252
|
+
client.isAlive = true;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
ws.on('close', (code, reason) => {
|
|
256
|
+
this.clients.delete(clientId);
|
|
257
|
+
this.logger.info('Client disconnected', {
|
|
258
|
+
id: clientId,
|
|
259
|
+
code,
|
|
260
|
+
reason: reason.toString(),
|
|
261
|
+
total: this.clients.size,
|
|
262
|
+
});
|
|
263
|
+
this.emit('client:disconnected', clientId);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
ws.on('error', (error) => {
|
|
267
|
+
this.logger.error('Client error', { id: clientId, error });
|
|
268
|
+
this.errors++;
|
|
269
|
+
this.clients.delete(clientId);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
this.emit('client:connected', clientId);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private async handleMessage(client: ClientConnection, data: RawData): Promise<void> {
|
|
277
|
+
client.lastActivity = new Date();
|
|
278
|
+
client.messageCount++;
|
|
279
|
+
this.messagesReceived++;
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const message = this.parseMessage(data);
|
|
283
|
+
|
|
284
|
+
if (!client.isAuthenticated && this.config.auth?.enabled) {
|
|
285
|
+
if (message.method !== 'authenticate') {
|
|
286
|
+
client.ws.send(this.serializeMessage({
|
|
287
|
+
jsonrpc: '2.0',
|
|
288
|
+
id: message.id || null,
|
|
289
|
+
error: { code: -32001, message: 'Authentication required' },
|
|
290
|
+
} as MCPResponse));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (message.jsonrpc !== '2.0') {
|
|
296
|
+
client.ws.send(this.serializeMessage({
|
|
297
|
+
jsonrpc: '2.0',
|
|
298
|
+
id: message.id || null,
|
|
299
|
+
error: { code: -32600, message: 'Invalid JSON-RPC version' },
|
|
300
|
+
} as MCPResponse));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (message.id === undefined) {
|
|
305
|
+
if (this.notificationHandler) {
|
|
306
|
+
await this.notificationHandler(message as MCPNotification);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
if (!this.requestHandler) {
|
|
310
|
+
client.ws.send(this.serializeMessage({
|
|
311
|
+
jsonrpc: '2.0',
|
|
312
|
+
id: message.id,
|
|
313
|
+
error: { code: -32603, message: 'No request handler' },
|
|
314
|
+
} as MCPResponse));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const startTime = performance.now();
|
|
319
|
+
const response = await this.requestHandler(message as MCPRequest);
|
|
320
|
+
const duration = performance.now() - startTime;
|
|
321
|
+
|
|
322
|
+
this.logger.debug('Request processed', {
|
|
323
|
+
clientId: client.id,
|
|
324
|
+
method: message.method,
|
|
325
|
+
duration: `${duration.toFixed(2)}ms`,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
client.ws.send(this.serializeMessage(response));
|
|
329
|
+
this.messagesSent++;
|
|
330
|
+
}
|
|
331
|
+
} catch (error) {
|
|
332
|
+
this.errors++;
|
|
333
|
+
this.logger.error('Message handling error', { clientId: client.id, error });
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
client.ws.send(this.serializeMessage({
|
|
337
|
+
jsonrpc: '2.0',
|
|
338
|
+
id: null,
|
|
339
|
+
error: { code: -32700, message: 'Parse error' },
|
|
340
|
+
} as MCPResponse));
|
|
341
|
+
} catch {
|
|
342
|
+
// Ignore send errors
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private parseMessage(data: RawData): any {
|
|
348
|
+
if (this.config.enableBinaryMode && Buffer.isBuffer(data)) {
|
|
349
|
+
return JSON.parse(data.toString());
|
|
350
|
+
}
|
|
351
|
+
return JSON.parse(data.toString());
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private serializeMessage(message: MCPResponse | MCPNotification): string | Buffer {
|
|
355
|
+
if (this.config.enableBinaryMode) {
|
|
356
|
+
return JSON.stringify(message);
|
|
357
|
+
}
|
|
358
|
+
return JSON.stringify(message);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private startHeartbeat(): void {
|
|
362
|
+
const interval = this.config.heartbeatInterval || 30000;
|
|
363
|
+
|
|
364
|
+
this.heartbeatTimer = setInterval(() => {
|
|
365
|
+
for (const client of this.clients.values()) {
|
|
366
|
+
if (!client.isAlive) {
|
|
367
|
+
this.logger.warn('Client heartbeat timeout', { id: client.id });
|
|
368
|
+
client.ws.terminate();
|
|
369
|
+
this.clients.delete(client.id);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
client.isAlive = false;
|
|
374
|
+
try {
|
|
375
|
+
client.ws.ping();
|
|
376
|
+
} catch {
|
|
377
|
+
// Ignore ping errors
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}, interval);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private stopHeartbeat(): void {
|
|
384
|
+
if (this.heartbeatTimer) {
|
|
385
|
+
clearInterval(this.heartbeatTimer);
|
|
386
|
+
this.heartbeatTimer = undefined;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function createWebSocketTransport(
|
|
392
|
+
logger: ILogger,
|
|
393
|
+
config: WebSocketTransportConfig
|
|
394
|
+
): WebSocketTransport {
|
|
395
|
+
return new WebSocketTransport(logger, config);
|
|
396
|
+
}
|