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
JavaScript
;
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