dj-postgres-mcp
Version:
Model Context Protocol (MCP) server for PostgreSQL database operations with centralized configuration via dj-config-mcp
186 lines (185 loc) • 6.54 kB
JavaScript
import { log } from '../shared/logger.js';
import { ManagedError, ErrorCodes } from '../shared/errors.js';
// PostgreSQL data type mappings
const PG_DATA_TYPES = {
16: 'bool',
17: 'bytea',
18: 'char',
19: 'name',
20: 'int8',
21: 'int2',
23: 'int4',
25: 'text',
26: 'oid',
114: 'json',
700: 'float4',
701: 'float8',
1000: '_bool',
1001: '_bytea',
1002: '_char',
1003: '_name',
1005: '_int2',
1007: '_int4',
1009: '_text',
1014: '_bpchar',
1015: '_varchar',
1016: '_int8',
1021: '_float4',
1022: '_float8',
1042: 'bpchar',
1043: 'varchar',
1082: 'date',
1083: 'time',
1114: 'timestamp',
1115: '_timestamp',
1184: 'timestamptz',
1185: '_timestamptz',
1186: 'interval',
1187: '_interval',
1231: '_numeric',
1263: '_cstring',
1700: 'numeric',
2950: 'uuid',
2951: '_uuid',
3802: 'jsonb',
3807: '_jsonb'
};
export class QueryExecutor {
async executeQuery(client, query, params) {
const startTime = Date.now();
try {
log('QueryExecutor: Executing query', {
command: query.substring(0, 50),
paramCount: params?.length || 0
});
const result = await client.query(query, params || []);
const executionTime = Date.now() - startTime;
const fields = result.fields?.map(field => ({
name: field.name,
dataTypeID: field.dataTypeID,
dataTypeName: PG_DATA_TYPES[field.dataTypeID] || `oid:${field.dataTypeID}`
})) || [];
log('QueryExecutor: Query completed', {
rowCount: result.rowCount,
executionTime,
command: result.command
});
return {
rows: result.rows,
rowCount: result.rowCount || 0,
command: result.command || 'UNKNOWN',
fields,
executionTime
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
// Categorize database errors
if (errorMessage.includes('permission denied')) {
throw new ManagedError(ErrorCodes.PERMISSION_DENIED, 'Permission denied for this operation', { query: query.substring(0, 100) });
}
else if (errorMessage.includes('does not exist')) {
throw new ManagedError(ErrorCodes.TABLE_NOT_FOUND, errorMessage, { query: query.substring(0, 100) });
}
else if (errorMessage.includes('syntax error')) {
throw new ManagedError(ErrorCodes.QUERY_ERROR, `SQL syntax error: ${errorMessage}`, { query: query.substring(0, 100) });
}
throw new ManagedError(ErrorCodes.QUERY_ERROR, `Query execution failed: ${errorMessage}`, {
query: query.substring(0, 100),
executionTime: Date.now() - startTime
});
}
}
async listTables(client, schema) {
let query = `
SELECT
schemaname as schema_name,
tablename as table_name,
tableowner as table_owner,
hasindexes as has_indexes,
hastriggers as has_triggers
FROM pg_tables
WHERE schemaname NOT IN ('information_schema', 'pg_catalog')
`;
const params = [];
if (schema) {
query += ' AND schemaname = $1';
params.push(schema);
}
query += ' ORDER BY schemaname, tablename';
const result = await client.query(query, params);
return result.rows.map(row => ({
schema_name: row.schema_name,
table_name: row.table_name,
table_owner: row.table_owner,
has_indexes: row.has_indexes,
has_triggers: row.has_triggers
}));
}
async describeTable(client, tableName, schema = 'public') {
// Get column information
const columnQuery = `
SELECT
column_name,
data_type,
is_nullable,
column_default,
character_maximum_length,
numeric_precision,
numeric_scale
FROM information_schema.columns
WHERE table_schema = $1 AND table_name = $2
ORDER BY ordinal_position
`;
const columnResult = await client.query(columnQuery, [schema, tableName]);
if (columnResult.rows.length === 0) {
throw new ManagedError(ErrorCodes.TABLE_NOT_FOUND, `Table "${schema}"."${tableName}" not found`);
}
// Get index information
const indexQuery = `
SELECT
indexname as index_name,
indexdef as index_definition
FROM pg_indexes
WHERE schemaname = $1 AND tablename = $2
`;
const indexResult = await client.query(indexQuery, [schema, tableName]);
// Get primary key information
const pkQuery = `
SELECT
kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
WHERE tc.constraint_type = 'PRIMARY KEY'
AND tc.table_schema = $1
AND tc.table_name = $2
`;
const pkResult = await client.query(pkQuery, [schema, tableName]);
const primaryKeyColumns = new Set(pkResult.rows.map(row => row.column_name));
// Build response
const columns = columnResult.rows.map(col => ({
column_name: col.column_name,
data_type: col.data_type,
is_nullable: col.is_nullable,
column_default: col.column_default,
character_maximum_length: col.character_maximum_length,
numeric_precision: col.numeric_precision,
numeric_scale: col.numeric_scale,
is_primary_key: primaryKeyColumns.has(col.column_name)
}));
const indexes = indexResult.rows.map(idx => ({
index_name: idx.index_name,
index_definition: idx.index_definition,
is_primary: primaryKeyColumns.size > 0 && idx.index_definition.includes([...primaryKeyColumns].join(', ')),
is_unique: idx.index_definition.toLowerCase().includes('unique')
}));
return {
schema,
table: tableName,
columns,
indexes
};
}
}