@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,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
|
+
}
|