UNPKG

postgres-mcp-tools

Version:

PostgreSQL-based memory system with vector search capabilities for AI applications, including MCP integration for Claude

195 lines (194 loc) 8.2 kB
import http from 'http'; import { RobustReadBuffer, serializeMessage } from './robust-stdio.js'; import { logger } from '../utils/logger.js'; import { handleError, tryCatch, createTransportError } from '../utils/error-handler.js'; /** * A robust HTTP transport for MCP that can handle non-JSON messages * Fully implements the Transport interface with proper message handling */ export class RobustHttpTransport { constructor() { this.server = null; this.readBuffer = new RobustReadBuffer(); // Clients to maintain bidirectional communication this.clients = new Map(); // Generate a unique session ID this.sessionId = Math.random().toString(36).substring(2, 15); logger.debug(`Created transport with session ID: ${this.sessionId}`); } /** * Start the transport * In this implementation, we initialize but don't create the HTTP server yet */ async start() { logger.debug('Transport initialized'); return Promise.resolve(); } /** * Close the transport and clean up resources */ async close() { return new Promise((resolve) => { if (!this.server) { logger.debug('Transport already closed'); resolve(); return; } // Close active connections for (const [id, res] of this.clients.entries()) { try { res.end(); this.clients.delete(id); logger.debug(`Closed connection to client ${id}`); } catch (error) { logger.error(`Error closing connection to client ${id}:`, error); } } // Close the server this.server.close(() => { logger.debug('HTTP server closed'); this.server = null; this.readBuffer.clear(); if (this.onclose) this.onclose(); resolve(); }); }); } /** * Send a message to connected clients * Properly implements the send method required by the Transport interface */ async send(message) { return tryCatch(async () => { const serialized = serializeMessage(message); if (this.clients.size === 0) { logger.debug(`No clients connected, buffering message: ${serialized.substring(0, 100)}${serialized.length > 100 ? '...' : ''}`); return Promise.resolve(); } // Send to all connected clients const sendPromises = []; for (const [id, res] of this.clients.entries()) { if (!res.writableEnded) { sendPromises.push(new Promise((resolve, reject) => { res.write(`${serialized}`, (err) => { if (err) { const transportError = createTransportError(`Error sending message to client ${id}: ${err.message}`, 'send'); if (this.onerror) this.onerror(transportError); reject(transportError); } else { resolve(); } }); })); } else { // Remove ended connections this.clients.delete(id); } } await Promise.all(sendPromises); logger.debug(`Sent message to ${sendPromises.length} clients`); return Promise.resolve(); }, 'Transport.send'); } /** * Process input data from clients * Handles JSON parsing errors gracefully */ processInput(data, clientId) { try { // Convert to buffer if needed const buffer = typeof data === 'string' ? Buffer.from(data) : data; // Add to the read buffer this.readBuffer.append(buffer); // Process all complete messages in the buffer let message; while ((message = this.readBuffer.readMessage()) !== null) { if (this.onmessage && message) { logger.debug(`Processing message from client ${clientId}`); this.onmessage(message); } } } catch (error) { // Use the standardized error handler const processedError = handleError(error, `processInput(clientId=${clientId})`); // Call the error handler if provided if (this.onerror) { this.onerror(processedError); } } } /** * Create and start the HTTP server * Sets up proper bidirectional communication with clients */ createServer(port, host) { return new Promise((resolve, reject) => { try { this.server = http.createServer((req, res) => { // Generate a unique client ID const clientId = Math.random().toString(36).substring(2, 15); // Set headers for proper HTTP streaming response res.setHeader('Content-Type', 'application/json-stream'); res.setHeader('Transfer-Encoding', 'chunked'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // Add client to the list of active connections this.clients.set(clientId, res); logger.debug(`New client connected: ${clientId}`); // Send a welcome message res.write(JSON.stringify({ type: 'connection', status: 'established', clientId }) + '\n'); // Handle client disconnect req.on('close', () => { this.clients.delete(clientId); logger.debug(`Client disconnected: ${clientId}`); }); // Process request body if present if (req.method === 'POST') { const chunks = []; req.on('data', (chunk) => chunks.push(chunk)); req.on('end', () => { try { const body = Buffer.concat(chunks); if (body.length > 0) { logger.debug(`Received data from client ${clientId}, length: ${body.length}`); this.processInput(body, clientId); } } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); logger.error(`Error handling request body from client ${clientId}:`, err); if (this.onerror) this.onerror(err); } }); } }); // Handle server errors this.server.on('error', (error) => { logger.error('HTTP server error:', error); if (this.onerror) this.onerror(error); reject(error); }); // Start the server this.server.listen(port, host, () => { logger.info(`HTTP transport listening on http://${host}:${port}`); resolve(); }); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); logger.error('Error creating HTTP server:', err); if (this.onerror) this.onerror(err); reject(err); } }); } }