UNPKG

postgres-mcp-tools

Version:

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

229 lines (201 loc) 7.66 kB
import http from 'http'; import { Transport } from '../../typescript-sdk/dist/esm/shared/transport.js'; import { RobustReadBuffer, serializeMessage } from './robust-stdio.js'; import { JSONRPCMessage } from '../../typescript-sdk/dist/esm/types.js'; import { logger } from '../utils/logger.js'; import { handleError, tryCatch, ErrorCodes, 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 implements Transport { private server: http.Server | null = null; private readBuffer = new RobustReadBuffer(); // Public properties as required by the Transport interface sessionId: string; // Callback handlers as required by the Transport interface onmessage?: (message: JSONRPCMessage) => void; onclose?: () => void; onerror?: (error: Error) => void; // Clients to maintain bidirectional communication private clients: Map<string, http.ServerResponse> = new Map(); constructor() { // 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(): Promise<void> { logger.debug('Transport initialized'); return Promise.resolve(); } /** * Close the transport and clean up resources */ async close(): Promise<void> { return new Promise<void>((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: JSONRPCMessage): Promise<void> { 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: Promise<void>[] = []; for (const [id, res] of this.clients.entries()) { if (!res.writableEnded) { sendPromises.push( new Promise<void>((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: Buffer | string, clientId: string): void { 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: JSONRPCMessage | null; 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: number, host: string): Promise<void> { return new Promise<void>((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: Buffer[] = []; 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); } }); } }