UNPKG

sequelae-mcp

Version:

Let Claude, Cursor, and other AI agents run real SQL queries on live Postgres databases. No more copy-pasting SQL, stale schema docs, or hallucinated DB adapters — just raw, real-time access. Now with MCP support!

482 lines 19.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.McpToolHandler = void 0; const sql_executor_1 = require("../core/sql-executor"); const tool_definition_1 = require("./tool-definition"); const packageJson = __importStar(require("../../package.json")); class McpToolHandler { constructor(connectionString) { this.connectionString = connectionString; this.executor = null; // Connection string can be provided later via environment } async handleToolCall(request) { try { // Validate input const validation = (0, tool_definition_1.validateToolInput)(request.tool, request.arguments); if (!validation.valid) { return this.errorResponse(validation.error || 'Invalid input'); } // Ensure we have a connection const connString = this.connectionString || process.env.DATABASE_URL; if (!connString) { return this.errorResponse('DATABASE_URL environment variable is not set'); } // Initialize executor if needed if (!this.executor) { this.executor = new sql_executor_1.SqlExecutor(connString); } // Route to appropriate handler switch (request.tool) { case 'sql_exec': return this.handleSqlExec(request.arguments); case 'sql_file': return this.handleSqlFile(request.arguments); case 'sql_schema': return this.handleSqlSchema(request.arguments); case 'sql_backup': return this.handleSqlBackup(request.arguments); case 'sql_health': return this.handleSqlHealth(request.arguments); default: return this.errorResponse(`Unknown tool: ${request.tool}`); } } catch (error) { return this.errorResponse(error instanceof Error ? error.message : String(error)); } } async handleSqlExec(args) { const query = args.query; const jsonMode = args.json !== false; // Default true const useTransaction = args.transaction !== false; // Default true const timeout = args.timeout; // Set timeout environment variable if provided if (timeout) { process.env.POSTGRES_STATEMENT_TIMEOUT = timeout.toString(); } try { if (!this.executor) { throw new Error('SqlExecutor not initialized'); } const result = await this.executor.executeQuery(query, useTransaction, timeout); if (jsonMode) { return { content: [ { type: 'text', text: JSON.stringify({ success: true, command: result.command, rowCount: result.rowCount, rows: result.rows, duration: result.duration, }, null, 2), }, ], }; } else { // Format as table-like text let text = `Command: ${result.command}\n`; text += `Rows: ${result.rowCount}\n`; text += `Duration: ${result.duration}ms\n\n`; if (result.rows && result.rows.length > 0) { // Simple table formatting const headers = Object.keys(result.rows[0]); text += headers.join(' | ') + '\n'; text += headers.map(() => '---').join(' | ') + '\n'; for (const row of result.rows) { text += headers.map(h => String(row[h] ?? '')).join(' | ') + '\n'; } } return { content: [ { type: 'text', text, }, ], }; } } catch (error) { const err = error; if (jsonMode) { return { content: [ { type: 'text', text: JSON.stringify({ success: false, error: err.message, position: err.position, }, null, 2), }, ], }; } else { return this.errorResponse(err.message); } } } async handleSqlFile(args) { const filepath = args.filepath; const jsonMode = args.json !== false; // Default true const useTransaction = args.transaction !== false; // Default true const timeout = args.timeout; // Set timeout environment variable if provided if (timeout) { process.env.POSTGRES_STATEMENT_TIMEOUT = timeout.toString(); } try { if (!this.executor) { throw new Error('SqlExecutor not initialized'); } const result = await this.executor.executeFile(filepath, useTransaction, timeout); if (jsonMode) { return { content: [ { type: 'text', text: JSON.stringify({ success: true, command: result.command, rowCount: result.rowCount, rows: result.rows, duration: result.duration, }, null, 2), }, ], }; } else { // Format as table-like text let text = `Command: ${result.command}\n`; text += `Rows: ${result.rowCount}\n`; text += `Duration: ${result.duration}ms\n\n`; if (result.rows && result.rows.length > 0) { // Simple table formatting const headers = Object.keys(result.rows[0]); text += headers.join(' | ') + '\n'; text += headers.map(() => '---').join(' | ') + '\n'; for (const row of result.rows) { text += headers.map(h => String(row[h] ?? '')).join(' | ') + '\n'; } } return { content: [ { type: 'text', text, }, ], }; } } catch (error) { return this.errorResponse(error instanceof Error ? error.message : String(error)); } } async handleSqlSchema(args) { const tables = args.tables || []; const allSchemas = args.allSchemas || false; const jsonMode = args.json !== false; // Default true try { if (!this.executor) { throw new Error('SqlExecutor not initialized'); } const result = await this.executor.getSchema(tables.length > 0 ? tables : undefined, allSchemas); if (jsonMode) { return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } else { // Format as readable text let text = 'DATABASE SCHEMA:\n\n'; for (const table of result.tables) { text += `📋 ${table.schema}.${table.name}\n`; text += ' Columns:\n'; for (const col of table.columns) { const nullable = col.is_nullable === 'YES' ? ' (nullable)' : ''; const dataType = col.character_maximum_length ? `${col.data_type}(${col.character_maximum_length})` : col.data_type; const defaultVal = col.column_default ? ` DEFAULT ${col.column_default}` : ''; text += ` - ${col.column_name}: ${dataType}${nullable}${defaultVal}\n`; } if (table.constraints.length > 0) { text += ' Constraints:\n'; const constraintsByType = table.constraints.reduce((acc, c) => { if (!acc[c.constraint_type]) acc[c.constraint_type] = []; acc[c.constraint_type].push(c.column_name); return acc; }, {}); for (const [type, columns] of Object.entries(constraintsByType)) { text += ` - ${type}: ${columns.join(', ')}\n`; } } text += '\n'; } if (result.missingTables && result.missingTables.length > 0) { text += '❌ TABLES NOT FOUND:\n'; for (const missing of result.missingTables) { text += ` - "${missing.table_name}"`; if (missing.suggestions.length > 0) { text += ` (Did you mean: ${missing.suggestions.join(', ')}?)`; } text += '\n'; } } return { content: [ { type: 'text', text, }, ], }; } } catch (error) { return this.errorResponse(error instanceof Error ? error.message : String(error)); } } async handleSqlBackup(args) { try { if (!this.executor) { throw new Error('SqlExecutor not initialized'); } // Validate mutually exclusive options if (args.dataOnly && args.schemaOnly) { return this.errorResponse('Cannot specify both dataOnly and schemaOnly options'); } // Validate format option const validFormats = ['plain', 'custom', 'directory', 'tar']; if (args.format && !validFormats.includes(args.format)) { return this.errorResponse(`Invalid format. Must be one of: ${validFormats.join(', ')}`); } const result = await this.executor.backup({ format: args.format, tables: args.tables, schemas: args.schemas, dataOnly: args.dataOnly, schemaOnly: args.schemaOnly, compress: args.compress, outputPath: args.outputPath, }); if (result.success) { const sizeInfo = result.size ? ` (${(result.size / 1024 / 1024).toFixed(2)} MB)` : ''; return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: `Backup completed successfully`, outputPath: result.outputPath, size: result.size, sizeFormatted: sizeInfo, duration: result.duration, durationFormatted: `${(result.duration / 1000).toFixed(2)}s`, }, null, 2), }, ], }; } else { return this.errorResponse(result.error || 'Backup failed'); } } catch (error) { return this.errorResponse(error instanceof Error ? error.message : String(error)); } } async handleSqlHealth(args) { const includeVersion = args.includeVersion !== false; // Default true const includeConnectionInfo = args.includeConnectionInfo !== false; // Default true const jsonMode = args.json !== false; // Default true try { if (!this.executor) { throw new Error('SqlExecutor not initialized'); } const healthInfo = { status: 'healthy', timestamp: new Date().toISOString(), }; // Test database connection try { const testResult = await this.executor.executeQuery('SELECT 1 as test', false); healthInfo.connectionTest = { success: true, latency: testResult.duration, }; } catch (error) { healthInfo.status = 'unhealthy'; healthInfo.connectionTest = { success: false, error: error instanceof Error ? error.message : 'Unknown error', }; } // Get database version if requested if (includeVersion && healthInfo.status === 'healthy') { try { const versionResult = await this.executor.executeQuery('SELECT version()', false); if (versionResult.rows && versionResult.rows.length > 0) { healthInfo.database = { version: String(versionResult.rows[0].version), }; } } catch (error) { healthInfo.database = { error: error instanceof Error ? error.message : 'Failed to get version', }; } } // Get connection pool info if requested if (includeConnectionInfo) { const poolManager = this.executor.poolManagerInstance; if (poolManager && poolManager.getStatus) { healthInfo.connectionPool = poolManager.getStatus(); } else { healthInfo.connectionPool = { initialized: false, total: 0, idle: 0, waiting: 0, maxConnections: 0, idleTimeout: 0, connectionTimeout: 0, note: 'Pool statistics not available', }; } } // Add tool information healthInfo.tool = { name: 'sequelae-mcp', version: packageJson.version, }; if (jsonMode) { return { content: [ { type: 'text', text: JSON.stringify(healthInfo, null, 2), }, ], }; } else { // Format as readable text let text = `Health Check Report\n`; text += `==================\n\n`; text += `Status: ${healthInfo.status.toUpperCase()}\n`; text += `Timestamp: ${healthInfo.timestamp}\n\n`; text += `Connection Test:\n`; text += ` Success: ${healthInfo.connectionTest.success}\n`; if (healthInfo.connectionTest.latency) { text += ` Latency: ${healthInfo.connectionTest.latency}ms\n`; } if (healthInfo.connectionTest.error) { text += ` Error: ${healthInfo.connectionTest.error}\n`; } if (healthInfo.database) { text += `\nDatabase:\n`; if (healthInfo.database.version) { text += ` Version: ${healthInfo.database.version}\n`; } if (healthInfo.database.error) { text += ` Error: ${healthInfo.database.error}\n`; } } if (healthInfo.connectionPool) { text += `\nConnection Pool:\n`; if (healthInfo.connectionPool.note) { text += ` ${healthInfo.connectionPool.note}\n`; } else { text += ` Total: ${healthInfo.connectionPool.total || 'N/A'}\n`; text += ` Idle: ${healthInfo.connectionPool.idle || 'N/A'}\n`; text += ` Waiting: ${healthInfo.connectionPool.waiting || 'N/A'}\n`; } } text += `\nTool:\n`; text += ` Name: ${healthInfo.tool.name}\n`; text += ` Version: ${healthInfo.tool.version}\n`; return { content: [ { type: 'text', text, }, ], }; } } catch (error) { return this.errorResponse(error instanceof Error ? error.message : String(error)); } } errorResponse(message) { return { content: [ { type: 'error', error: message, }, ], }; } async close() { if (this.executor) { await this.executor.close(); this.executor = null; } } } exports.McpToolHandler = McpToolHandler; //# sourceMappingURL=tool-handler.js.map