UNPKG

@sethdouglasford/claude-flow

Version:

Claude Code Flow - Advanced AI-powered development workflows with SPARC methodology

399 lines 13.7 kB
/** * HTTP transport for MCP */ import express from "express"; import { createServer } from "node:http"; import { WebSocketServer, WebSocket } from "ws"; import cors from "cors"; import helmet from "helmet"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { MCPTransportError } from "../../utils/errors.js"; /** * HTTP transport implementation */ export class HttpTransport { host; port; tlsEnabled; logger; config; requestHandler; notificationHandler; app; server; wss; messageCount = 0; notificationCount = 0; running = false; connections = new Set(); activeWebSockets = new Set(); constructor(host, port, tlsEnabled, logger, config) { this.host = host; this.port = port; this.tlsEnabled = tlsEnabled; this.logger = logger; this.config = config; this.app = express(); this.setupMiddleware(); this.setupRoutes(); } async start() { if (this.running) { throw new MCPTransportError("Transport already running"); } this.logger.info("Starting HTTP transport", { host: this.host, port: this.port, tls: this.tlsEnabled, }); try { // Create HTTP server this.server = createServer(this.app); // Create WebSocket server this.wss = new WebSocketServer({ server: this.server, path: "/ws", }); this.setupWebSocketHandlers(); // Start server await new Promise((resolve, reject) => { this.server.listen(this.port, this.host, () => { this.logger.info(`HTTP server listening on ${this.host}:${this.port}`); resolve(); }); this.server.on("error", reject); }); this.running = true; this.logger.info("HTTP transport started"); } catch (error) { throw new MCPTransportError("Failed to start HTTP transport", { error }); } } async stop() { if (!this.running) { return; } this.logger.info("Stopping HTTP transport"); this.running = false; // Close all WebSocket connections for (const ws of this.activeWebSockets) { try { ws.close(); } catch { // Ignore errors } } this.activeWebSockets.clear(); this.connections.clear(); // Close WebSocket server if (this.wss) { this.wss.close(); this.wss = undefined; } // Shutdown HTTP server if (this.server) { await new Promise((resolve) => { this.server.close(() => resolve()); }); this.server = undefined; } this.logger.info("HTTP transport stopped"); } onRequest(handler) { this.requestHandler = handler; } onNotification(handler) { this.notificationHandler = handler; } async getHealthStatus() { return { healthy: this.running, metrics: { messagesReceived: this.messageCount, notificationsSent: this.notificationCount, activeConnections: this.connections.size, activeWebSockets: this.activeWebSockets.size, }, }; } setupMiddleware() { // Security middleware this.app.use(helmet()); // CORS middleware if (this.config?.corsEnabled) { const origins = this.config.corsOrigins ?? ["*"]; this.app.use(cors({ origin: origins, credentials: true, maxAge: 86400, // 24 hours })); } // Body parsing middleware this.app.use(express.json({ limit: "10mb" })); this.app.use(express.text()); } setupRoutes() { // Get current file directory for static files const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const consoleDir = join(__dirname, "../../ui/console"); // Serve static files for the web console this.app.use("/console", express.static(consoleDir)); // Web console route this.app.get("/", (req, res) => { res.redirect("/console"); }); this.app.get("/console", (req, res) => { res.sendFile(join(consoleDir, "index.html")); }); // Health check endpoint this.app.get("/health", (req, res) => { res.json({ status: "ok", timestamp: new Date().toISOString() }); }); // MCP JSON-RPC endpoint this.app.post("/rpc", async (req, res) => { await this.handleJsonRpcRequest(req, res); }); // Handle preflight requests this.app.options("*", (req, res) => { res.status(204).end(); }); // 404 handler this.app.use((req, res) => { res.status(404).json({ error: "Not found" }); }); // Error handler this.app.use((err, req, res, next) => { this.logger.error("Express error", err); const errorMessage = err instanceof Error ? err.message : "Unknown error occurred"; res.status(500).json({ error: "Internal server error", message: errorMessage, }); }); } setupWebSocketHandlers() { if (!this.wss) return; this.wss.on("connection", (ws, req) => { this.activeWebSockets.add(ws); this.logger.info("WebSocket client connected", { totalClients: this.activeWebSockets.size, }); ws.on("close", () => { this.activeWebSockets.delete(ws); this.logger.info("WebSocket client disconnected", { totalClients: this.activeWebSockets.size, }); }); ws.on("error", (error) => { this.logger.error("WebSocket error", error); this.activeWebSockets.delete(ws); }); ws.on("message", async (data) => { try { const message = JSON.parse(String(data)); if (message.id === undefined) { // Notification from client await this.handleNotificationMessage(message); } else { // Request from client const response = await this.handleRequestMessage(message); ws.send(JSON.stringify(response)); } } catch (error) { this.logger.error("Error processing WebSocket message", error); // Send error response if it was a request try { const parsed = JSON.parse(String(data)); if (parsed.id !== undefined) { ws.send(JSON.stringify({ jsonrpc: "2.0", id: parsed.id, error: { code: -32603, message: "Internal error", }, })); } } catch { // Ignore parse errors for error responses } } }); }); } async handleJsonRpcRequest(req, res) { // Check content type if (!req.is("application/json")) { res.status(400).json({ jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid content type - expected application/json", }, }); return; } // Check authorization if authentication is enabled if (this.config?.auth?.enabled) { const authResult = await this.validateAuth(req); if (!authResult.valid) { res.status(401).json({ error: authResult.error || "Unauthorized", }); return; } } try { const mcpMessage = req.body; // Validate JSON-RPC format if (!mcpMessage.jsonrpc || mcpMessage.jsonrpc !== "2.0") { res.status(400).json({ jsonrpc: "2.0", id: mcpMessage.id || null, error: { code: -32600, message: "Invalid request - missing or invalid jsonrpc version", }, }); return; } if (!mcpMessage.method) { res.status(400).json({ jsonrpc: "2.0", id: mcpMessage.id || null, error: { code: -32600, message: "Invalid request - missing method", }, }); return; } this.messageCount++; // Check if this is a notification (no id) or request if (mcpMessage.id === undefined) { // Handle notification await this.handleNotificationMessage(mcpMessage); // Notifications don't get responses res.status(204).end(); } else { // Handle request const response = await this.handleRequestMessage(mcpMessage); res.json(response); } } catch (error) { this.logger.error("Error handling JSON-RPC request", error); res.status(500).json({ jsonrpc: "2.0", id: null, error: { code: -32603, message: "Internal error", data: error instanceof Error ? error.message : String(error), }, }); } } async handleRequestMessage(request) { if (!this.requestHandler) { return { jsonrpc: "2.0", id: request.id, error: { code: -32603, message: "No request handler registered", }, }; } try { return await this.requestHandler(request); } catch (error) { this.logger.error("Request handler error", { request, error }); return { jsonrpc: "2.0", id: request.id, error: { code: -32603, message: "Internal error", data: error instanceof Error ? error.message : String(error), }, }; } } async handleNotificationMessage(notification) { if (!this.notificationHandler) { this.logger.warn("Received notification but no handler registered", { method: notification.method, }); return; } try { await this.notificationHandler(notification); } catch (error) { this.logger.error("Notification handler error", { notification, error }); // Notifications don't send error responses } } async validateAuth(req) { const auth = req.headers.authorization; if (!auth) { return { valid: false, error: "Authorization header required" }; } // Extract token from Authorization header const tokenMatch = auth.match(/^Bearer\s+(.+)$/i); if (!tokenMatch) { return { valid: false, error: "Invalid authorization format - use Bearer token" }; } const token = tokenMatch[1]; // Validate against configured tokens if (this.config?.auth?.tokens && this.config.auth.tokens.length > 0) { const isValid = this.config.auth.tokens.includes(token); if (!isValid) { return { valid: false, error: "Invalid token" }; } } return { valid: true }; } async connect() { // For HTTP transport, connect is handled by start() if (!this.running) { await this.start(); } } async disconnect() { // For HTTP transport, disconnect is handled by stop() await this.stop(); } async sendRequest(request) { // HTTP transport is server-side, it doesn't send requests throw new Error("HTTP transport does not support sending requests"); } async sendNotification(notification) { // Broadcast notification to all connected WebSocket clients const message = JSON.stringify(notification); for (const ws of this.activeWebSockets) { try { if (ws.readyState === WebSocket.OPEN) { ws.send(message); } } catch (error) { this.logger.error("Failed to send notification to WebSocket", error); } } this.notificationCount++; } } //# sourceMappingURL=http.js.map