@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.
Files changed (92) hide show
  1. package/.agentic-flow/intelligence.json +16 -0
  2. package/README.md +428 -0
  3. package/__tests__/integration.test.ts +449 -0
  4. package/__tests__/mcp.test.ts +641 -0
  5. package/dist/connection-pool.d.ts +36 -0
  6. package/dist/connection-pool.d.ts.map +1 -0
  7. package/dist/connection-pool.js +273 -0
  8. package/dist/connection-pool.js.map +1 -0
  9. package/dist/index.d.ts +75 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +85 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/oauth.d.ts +146 -0
  14. package/dist/oauth.d.ts.map +1 -0
  15. package/dist/oauth.js +318 -0
  16. package/dist/oauth.js.map +1 -0
  17. package/dist/prompt-registry.d.ts +90 -0
  18. package/dist/prompt-registry.d.ts.map +1 -0
  19. package/dist/prompt-registry.js +209 -0
  20. package/dist/prompt-registry.js.map +1 -0
  21. package/dist/rate-limiter.d.ts +86 -0
  22. package/dist/rate-limiter.d.ts.map +1 -0
  23. package/dist/rate-limiter.js +197 -0
  24. package/dist/rate-limiter.js.map +1 -0
  25. package/dist/resource-registry.d.ts +144 -0
  26. package/dist/resource-registry.d.ts.map +1 -0
  27. package/dist/resource-registry.js +405 -0
  28. package/dist/resource-registry.js.map +1 -0
  29. package/dist/sampling.d.ts +102 -0
  30. package/dist/sampling.d.ts.map +1 -0
  31. package/dist/sampling.js +268 -0
  32. package/dist/sampling.js.map +1 -0
  33. package/dist/schema-validator.d.ts +30 -0
  34. package/dist/schema-validator.d.ts.map +1 -0
  35. package/dist/schema-validator.js +182 -0
  36. package/dist/schema-validator.js.map +1 -0
  37. package/dist/server.d.ts +122 -0
  38. package/dist/server.d.ts.map +1 -0
  39. package/dist/server.js +829 -0
  40. package/dist/server.js.map +1 -0
  41. package/dist/session-manager.d.ts +55 -0
  42. package/dist/session-manager.d.ts.map +1 -0
  43. package/dist/session-manager.js +252 -0
  44. package/dist/session-manager.js.map +1 -0
  45. package/dist/task-manager.d.ts +81 -0
  46. package/dist/task-manager.d.ts.map +1 -0
  47. package/dist/task-manager.js +337 -0
  48. package/dist/task-manager.js.map +1 -0
  49. package/dist/tool-registry.d.ts +88 -0
  50. package/dist/tool-registry.d.ts.map +1 -0
  51. package/dist/tool-registry.js +353 -0
  52. package/dist/tool-registry.js.map +1 -0
  53. package/dist/transport/http.d.ts +55 -0
  54. package/dist/transport/http.d.ts.map +1 -0
  55. package/dist/transport/http.js +446 -0
  56. package/dist/transport/http.js.map +1 -0
  57. package/dist/transport/index.d.ts +50 -0
  58. package/dist/transport/index.d.ts.map +1 -0
  59. package/dist/transport/index.js +181 -0
  60. package/dist/transport/index.js.map +1 -0
  61. package/dist/transport/stdio.d.ts +43 -0
  62. package/dist/transport/stdio.d.ts.map +1 -0
  63. package/dist/transport/stdio.js +194 -0
  64. package/dist/transport/stdio.js.map +1 -0
  65. package/dist/transport/websocket.d.ts +65 -0
  66. package/dist/transport/websocket.d.ts.map +1 -0
  67. package/dist/transport/websocket.js +314 -0
  68. package/dist/transport/websocket.js.map +1 -0
  69. package/dist/types.d.ts +473 -0
  70. package/dist/types.d.ts.map +1 -0
  71. package/dist/types.js +40 -0
  72. package/dist/types.js.map +1 -0
  73. package/package.json +42 -0
  74. package/src/connection-pool.ts +344 -0
  75. package/src/index.ts +253 -0
  76. package/src/oauth.ts +447 -0
  77. package/src/prompt-registry.ts +296 -0
  78. package/src/rate-limiter.ts +266 -0
  79. package/src/resource-registry.ts +530 -0
  80. package/src/sampling.ts +363 -0
  81. package/src/schema-validator.ts +213 -0
  82. package/src/server.ts +1134 -0
  83. package/src/session-manager.ts +339 -0
  84. package/src/task-manager.ts +427 -0
  85. package/src/tool-registry.ts +475 -0
  86. package/src/transport/http.ts +532 -0
  87. package/src/transport/index.ts +233 -0
  88. package/src/transport/stdio.ts +252 -0
  89. package/src/transport/websocket.ts +396 -0
  90. package/src/types.ts +664 -0
  91. package/tsconfig.json +20 -0
  92. package/vitest.config.ts +13 -0
@@ -0,0 +1,532 @@
1
+ /**
2
+ * @claude-flow/mcp - HTTP Transport
3
+ *
4
+ * HTTP/REST transport with WebSocket support
5
+ */
6
+
7
+ import { EventEmitter } from 'events';
8
+ import express, { Express, Request, Response, NextFunction } from 'express';
9
+ import { createServer, Server } from 'http';
10
+ import { WebSocketServer, WebSocket } from 'ws';
11
+ import cors from 'cors';
12
+ import helmet from 'helmet';
13
+ import type {
14
+ ITransport,
15
+ TransportType,
16
+ MCPRequest,
17
+ MCPResponse,
18
+ MCPNotification,
19
+ RequestHandler,
20
+ NotificationHandler,
21
+ TransportHealthStatus,
22
+ ILogger,
23
+ AuthConfig,
24
+ } from '../types.js';
25
+
26
+ export interface HttpTransportConfig {
27
+ host: string;
28
+ port: number;
29
+ tlsEnabled?: boolean;
30
+ tlsCert?: string;
31
+ tlsKey?: string;
32
+ corsEnabled?: boolean;
33
+ corsOrigins?: string[];
34
+ auth?: AuthConfig;
35
+ maxRequestSize?: string;
36
+ requestTimeout?: number;
37
+ }
38
+
39
+ export class HttpTransport extends EventEmitter implements ITransport {
40
+ public readonly type: TransportType = 'http';
41
+
42
+ private requestHandler?: RequestHandler;
43
+ private notificationHandler?: NotificationHandler;
44
+ private app: Express;
45
+ private server?: Server;
46
+ private wss?: WebSocketServer;
47
+ private running = false;
48
+ private activeConnections = new Set<WebSocket>();
49
+
50
+ private messagesReceived = 0;
51
+ private messagesSent = 0;
52
+ private errors = 0;
53
+ private httpRequests = 0;
54
+ private wsMessages = 0;
55
+
56
+ constructor(
57
+ private readonly logger: ILogger,
58
+ private readonly config: HttpTransportConfig
59
+ ) {
60
+ super();
61
+ this.app = express();
62
+ this.setupMiddleware();
63
+ this.setupRoutes();
64
+ }
65
+
66
+ async start(): Promise<void> {
67
+ if (this.running) {
68
+ throw new Error('HTTP transport already running');
69
+ }
70
+
71
+ this.logger.info('Starting HTTP transport', {
72
+ host: this.config.host,
73
+ port: this.config.port,
74
+ });
75
+
76
+ this.server = createServer(this.app);
77
+
78
+ this.wss = new WebSocketServer({
79
+ server: this.server,
80
+ path: '/ws',
81
+ });
82
+
83
+ this.setupWebSocketHandlers();
84
+
85
+ await new Promise<void>((resolve, reject) => {
86
+ this.server!.listen(this.config.port, this.config.host, () => {
87
+ resolve();
88
+ });
89
+ this.server!.on('error', reject);
90
+ });
91
+
92
+ this.running = true;
93
+ this.logger.info('HTTP transport started', {
94
+ url: `http://${this.config.host}:${this.config.port}`,
95
+ });
96
+ }
97
+
98
+ async stop(): Promise<void> {
99
+ if (!this.running) {
100
+ return;
101
+ }
102
+
103
+ this.logger.info('Stopping HTTP transport');
104
+ this.running = false;
105
+
106
+ for (const ws of this.activeConnections) {
107
+ try {
108
+ ws.close(1000, 'Server shutting down');
109
+ } catch {
110
+ // Ignore errors
111
+ }
112
+ }
113
+ this.activeConnections.clear();
114
+
115
+ if (this.wss) {
116
+ this.wss.close();
117
+ this.wss = undefined;
118
+ }
119
+
120
+ if (this.server) {
121
+ await new Promise<void>((resolve) => {
122
+ this.server!.close(() => resolve());
123
+ });
124
+ this.server = undefined;
125
+ }
126
+
127
+ this.logger.info('HTTP transport stopped');
128
+ }
129
+
130
+ onRequest(handler: RequestHandler): void {
131
+ this.requestHandler = handler;
132
+ }
133
+
134
+ onNotification(handler: NotificationHandler): void {
135
+ this.notificationHandler = handler;
136
+ }
137
+
138
+ async getHealthStatus(): Promise<TransportHealthStatus> {
139
+ return {
140
+ healthy: this.running,
141
+ metrics: {
142
+ messagesReceived: this.messagesReceived,
143
+ messagesSent: this.messagesSent,
144
+ errors: this.errors,
145
+ httpRequests: this.httpRequests,
146
+ wsMessages: this.wsMessages,
147
+ activeConnections: this.activeConnections.size,
148
+ },
149
+ };
150
+ }
151
+
152
+ async sendNotification(notification: MCPNotification): Promise<void> {
153
+ const message = JSON.stringify(notification);
154
+
155
+ for (const ws of this.activeConnections) {
156
+ try {
157
+ if (ws.readyState === WebSocket.OPEN) {
158
+ ws.send(message);
159
+ this.messagesSent++;
160
+ }
161
+ } catch (error) {
162
+ this.logger.error('Failed to send notification', { error });
163
+ this.errors++;
164
+ }
165
+ }
166
+ }
167
+
168
+ private setupMiddleware(): void {
169
+ this.app.use(helmet({
170
+ contentSecurityPolicy: false,
171
+ }));
172
+
173
+ if (this.config.corsEnabled !== false) {
174
+ const allowedOrigins = this.config.corsOrigins;
175
+
176
+ if (!allowedOrigins || allowedOrigins.length === 0) {
177
+ this.logger.warn('CORS: No origins configured, restricting to same-origin only');
178
+ }
179
+
180
+ this.app.use(cors({
181
+ origin: (origin, callback) => {
182
+ if (!origin) {
183
+ callback(null, true);
184
+ return;
185
+ }
186
+
187
+ if (allowedOrigins && allowedOrigins.length > 0) {
188
+ if (allowedOrigins.includes(origin) || allowedOrigins.includes('*')) {
189
+ callback(null, true);
190
+ } else {
191
+ callback(new Error(`CORS: Origin '${origin}' not allowed`));
192
+ }
193
+ } else {
194
+ callback(new Error('CORS: Cross-origin requests not allowed'));
195
+ }
196
+ },
197
+ credentials: true,
198
+ maxAge: 86400,
199
+ methods: ['GET', 'POST', 'OPTIONS'],
200
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
201
+ }));
202
+ }
203
+
204
+ this.app.use(express.json({
205
+ limit: this.config.maxRequestSize || '10mb',
206
+ }));
207
+
208
+ if (this.config.requestTimeout) {
209
+ this.app.use((req, res, next) => {
210
+ res.setTimeout(this.config.requestTimeout!, () => {
211
+ res.status(408).json({
212
+ jsonrpc: '2.0',
213
+ id: null,
214
+ error: { code: -32000, message: 'Request timeout' },
215
+ });
216
+ });
217
+ next();
218
+ });
219
+ }
220
+
221
+ this.app.use((req, res, next) => {
222
+ const startTime = performance.now();
223
+ res.on('finish', () => {
224
+ const duration = performance.now() - startTime;
225
+ this.logger.debug('HTTP request', {
226
+ method: req.method,
227
+ path: req.path,
228
+ status: res.statusCode,
229
+ duration: `${duration.toFixed(2)}ms`,
230
+ });
231
+ });
232
+ next();
233
+ });
234
+ }
235
+
236
+ private setupRoutes(): void {
237
+ this.app.get('/health', (req, res) => {
238
+ res.json({
239
+ status: 'ok',
240
+ timestamp: new Date().toISOString(),
241
+ connections: this.activeConnections.size,
242
+ });
243
+ });
244
+
245
+ this.app.post('/rpc', async (req, res) => {
246
+ await this.handleHttpRequest(req, res);
247
+ });
248
+
249
+ this.app.post('/mcp', async (req, res) => {
250
+ await this.handleHttpRequest(req, res);
251
+ });
252
+
253
+ this.app.get('/info', (req, res) => {
254
+ res.json({
255
+ name: 'Claude-Flow MCP Server V3',
256
+ version: '3.0.0',
257
+ transport: 'http',
258
+ capabilities: {
259
+ jsonrpc: true,
260
+ websocket: true,
261
+ },
262
+ });
263
+ });
264
+
265
+ this.app.use((req, res) => {
266
+ res.status(404).json({
267
+ error: 'Not found',
268
+ path: req.path,
269
+ });
270
+ });
271
+
272
+ this.app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
273
+ this.logger.error('Express error', { error: err });
274
+ this.errors++;
275
+ res.status(500).json({
276
+ jsonrpc: '2.0',
277
+ id: null,
278
+ error: { code: -32603, message: 'Internal error' },
279
+ });
280
+ });
281
+ }
282
+
283
+ private setupWebSocketHandlers(): void {
284
+ if (!this.wss) return;
285
+
286
+ // SECURITY: Handle WebSocket authentication via upgrade request
287
+ this.wss.on('connection', (ws, req) => {
288
+ // Validate authentication if enabled
289
+ if (this.config.auth?.enabled) {
290
+ const url = new URL(req.url || '', `http://${req.headers.host}`);
291
+ const token = url.searchParams.get('token') || req.headers['authorization']?.replace(/^Bearer\s+/i, '');
292
+
293
+ if (!token) {
294
+ this.logger.warn('WebSocket connection rejected: no authentication token');
295
+ ws.close(4001, 'Authentication required');
296
+ return;
297
+ }
298
+
299
+ // SECURITY: Timing-safe token validation
300
+ let valid = false;
301
+ if (this.config.auth.tokens?.length) {
302
+ for (const validToken of this.config.auth.tokens) {
303
+ if (this.timingSafeCompare(token, validToken)) {
304
+ valid = true;
305
+ break;
306
+ }
307
+ }
308
+ }
309
+
310
+ if (!valid) {
311
+ this.logger.warn('WebSocket connection rejected: invalid token');
312
+ ws.close(4003, 'Invalid token');
313
+ return;
314
+ }
315
+ }
316
+
317
+ this.activeConnections.add(ws);
318
+ this.logger.info('WebSocket client connected', {
319
+ total: this.activeConnections.size,
320
+ authenticated: !!this.config.auth?.enabled,
321
+ });
322
+
323
+ ws.on('message', async (data) => {
324
+ await this.handleWebSocketMessage(ws, data.toString());
325
+ });
326
+
327
+ ws.on('close', () => {
328
+ this.activeConnections.delete(ws);
329
+ this.logger.info('WebSocket client disconnected', {
330
+ total: this.activeConnections.size,
331
+ });
332
+ });
333
+
334
+ ws.on('error', (error) => {
335
+ this.logger.error('WebSocket error', { error });
336
+ this.errors++;
337
+ this.activeConnections.delete(ws);
338
+ });
339
+ });
340
+ }
341
+
342
+ private async handleHttpRequest(req: Request, res: Response): Promise<void> {
343
+ this.httpRequests++;
344
+ this.messagesReceived++;
345
+
346
+ const requiresAuth = this.config.auth?.enabled !== false;
347
+
348
+ if (requiresAuth && this.config.auth) {
349
+ const authResult = this.validateAuth(req);
350
+ if (!authResult.valid) {
351
+ this.logger.warn('Authentication failed', {
352
+ ip: req.ip,
353
+ path: req.path,
354
+ error: authResult.error,
355
+ });
356
+ res.status(401).json({
357
+ jsonrpc: '2.0',
358
+ id: null,
359
+ error: { code: -32001, message: 'Unauthorized' },
360
+ });
361
+ return;
362
+ }
363
+ } else if (requiresAuth && !this.config.auth) {
364
+ this.logger.warn('No authentication configured - running in development mode');
365
+ }
366
+
367
+ const message = req.body;
368
+
369
+ if (message.jsonrpc !== '2.0') {
370
+ res.status(400).json({
371
+ jsonrpc: '2.0',
372
+ id: message.id || null,
373
+ error: { code: -32600, message: 'Invalid JSON-RPC version' },
374
+ });
375
+ return;
376
+ }
377
+
378
+ if (!message.method) {
379
+ res.status(400).json({
380
+ jsonrpc: '2.0',
381
+ id: message.id || null,
382
+ error: { code: -32600, message: 'Missing method' },
383
+ });
384
+ return;
385
+ }
386
+
387
+ if (message.id === undefined) {
388
+ if (this.notificationHandler) {
389
+ await this.notificationHandler(message as MCPNotification);
390
+ }
391
+ res.status(204).end();
392
+ } else {
393
+ if (!this.requestHandler) {
394
+ res.status(500).json({
395
+ jsonrpc: '2.0',
396
+ id: message.id,
397
+ error: { code: -32603, message: 'No request handler' },
398
+ });
399
+ return;
400
+ }
401
+
402
+ try {
403
+ const response = await this.requestHandler(message as MCPRequest);
404
+ res.json(response);
405
+ this.messagesSent++;
406
+ } catch (error) {
407
+ this.errors++;
408
+ res.status(500).json({
409
+ jsonrpc: '2.0',
410
+ id: message.id,
411
+ error: {
412
+ code: -32603,
413
+ message: error instanceof Error ? error.message : 'Internal error',
414
+ },
415
+ });
416
+ }
417
+ }
418
+ }
419
+
420
+ private async handleWebSocketMessage(ws: WebSocket, data: string): Promise<void> {
421
+ this.wsMessages++;
422
+ this.messagesReceived++;
423
+
424
+ try {
425
+ const message = JSON.parse(data);
426
+
427
+ if (message.jsonrpc !== '2.0') {
428
+ ws.send(JSON.stringify({
429
+ jsonrpc: '2.0',
430
+ id: message.id || null,
431
+ error: { code: -32600, message: 'Invalid JSON-RPC version' },
432
+ }));
433
+ return;
434
+ }
435
+
436
+ if (message.id === undefined) {
437
+ if (this.notificationHandler) {
438
+ await this.notificationHandler(message as MCPNotification);
439
+ }
440
+ } else {
441
+ if (!this.requestHandler) {
442
+ ws.send(JSON.stringify({
443
+ jsonrpc: '2.0',
444
+ id: message.id,
445
+ error: { code: -32603, message: 'No request handler' },
446
+ }));
447
+ return;
448
+ }
449
+
450
+ const response = await this.requestHandler(message as MCPRequest);
451
+ ws.send(JSON.stringify(response));
452
+ this.messagesSent++;
453
+ }
454
+ } catch (error) {
455
+ this.errors++;
456
+ this.logger.error('WebSocket message error', { error });
457
+
458
+ try {
459
+ const parsed = JSON.parse(data);
460
+ ws.send(JSON.stringify({
461
+ jsonrpc: '2.0',
462
+ id: parsed.id || null,
463
+ error: { code: -32700, message: 'Parse error' },
464
+ }));
465
+ } catch {
466
+ ws.send(JSON.stringify({
467
+ jsonrpc: '2.0',
468
+ id: null,
469
+ error: { code: -32700, message: 'Parse error' },
470
+ }));
471
+ }
472
+ }
473
+ }
474
+
475
+ /**
476
+ * SECURITY: Timing-safe token comparison to prevent timing attacks
477
+ */
478
+ private timingSafeCompare(a: string, b: string): boolean {
479
+ const crypto = require('crypto');
480
+
481
+ // Ensure both strings are the same length for timing-safe comparison
482
+ const bufA = Buffer.from(a, 'utf-8');
483
+ const bufB = Buffer.from(b, 'utf-8');
484
+
485
+ // If lengths differ, still do a comparison to prevent length-based timing
486
+ if (bufA.length !== bufB.length) {
487
+ // Compare against itself to maintain constant time
488
+ crypto.timingSafeEqual(bufA, bufA);
489
+ return false;
490
+ }
491
+
492
+ return crypto.timingSafeEqual(bufA, bufB);
493
+ }
494
+
495
+ private validateAuth(req: Request): { valid: boolean; error?: string } {
496
+ const auth = req.headers.authorization;
497
+
498
+ if (!auth) {
499
+ return { valid: false, error: 'Authorization header required' };
500
+ }
501
+
502
+ const tokenMatch = auth.match(/^Bearer\s+(.+)$/i);
503
+ if (!tokenMatch) {
504
+ return { valid: false, error: 'Invalid authorization format' };
505
+ }
506
+
507
+ const token = tokenMatch[1];
508
+
509
+ if (this.config.auth?.tokens?.length) {
510
+ // SECURITY: Use timing-safe comparison to prevent timing attacks
511
+ let valid = false;
512
+ for (const validToken of this.config.auth.tokens) {
513
+ if (this.timingSafeCompare(token, validToken)) {
514
+ valid = true;
515
+ break;
516
+ }
517
+ }
518
+ if (!valid) {
519
+ return { valid: false, error: 'Invalid token' };
520
+ }
521
+ }
522
+
523
+ return { valid: true };
524
+ }
525
+ }
526
+
527
+ export function createHttpTransport(
528
+ logger: ILogger,
529
+ config: HttpTransportConfig
530
+ ): HttpTransport {
531
+ return new HttpTransport(logger, config);
532
+ }