Skip to main content
Glama

MCP SQL Server

by ryudg
postgresql.adapter.ts10.6 kB
import { BaseDatabaseAdapter } from './base.adapter.js'; import { DatabaseConfig, QueryResult, TableInfo, QueryOptions, DatabaseType, SchemaInfo, ColumnInfo, } from '../../types/database.types.js'; import { Logger } from '../../core/logger.js'; // Mock pg module for testing purposes interface MockClient { query: (text: string, values?: any[]) => Promise<MockQueryResult>; connect: () => Promise<void>; end: () => Promise<void>; } interface MockQueryResult { rows: any[]; rowCount: number; command: string; fields?: any[]; } interface MockPool { connect: () => Promise<MockClient>; end: () => Promise<void>; query: (text: string, values?: any[]) => Promise<MockQueryResult>; } interface MockPg { Pool: new (config: any) => MockPool; Client: new (config: any) => MockClient; } // This will be mocked in tests export const defaultPg: MockPg = { Pool: class { constructor(config: any) { throw new Error('PostgreSQL adapter requires pg library to be installed'); } async connect() { throw new Error('PostgreSQL adapter requires pg library to be installed'); } async end() { throw new Error('PostgreSQL adapter requires pg library to be installed'); } async query() { throw new Error('PostgreSQL adapter requires pg library to be installed'); } } as any, Client: class { constructor(config: any) { throw new Error('PostgreSQL adapter requires pg library to be installed'); } async connect() { throw new Error('PostgreSQL adapter requires pg library to be installed'); } async end() { throw new Error('PostgreSQL adapter requires pg library to be installed'); } async query() { throw new Error('PostgreSQL adapter requires pg library to be installed'); } } as any, }; /** * PostgreSQL Database Adapter */ export class PostgreSQLAdapter extends BaseDatabaseAdapter { private pool: MockPool | null = null; private pg: MockPg; constructor(config: DatabaseConfig, logger?: Logger, pg?: MockPg) { super(config, logger); this.pg = pg || defaultPg; } /** * Connect to database */ async connect(): Promise<void> { try { const config = { host: this.config.host, port: this.config.port || 5432, database: this.config.database, user: this.config.user, password: this.config.password, ssl: this.config.ssl || false, connectionTimeoutMillis: this.config.options?.connectionTimeout || 60000, idleTimeoutMillis: this.config.options?.requestTimeout || 60000, max: 20, }; this.pool = new this.pg.Pool(config); this.connected = true; } catch (error) { this.connected = false; throw new Error( `PostgreSQL connection failed: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Disconnect from database */ async disconnect(): Promise<void> { try { if (this.pool) { await this.pool.end(); this.pool = null; } this.connected = false; } catch (error) { throw new Error( `PostgreSQL disconnection failed: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Execute query */ async executeQuery(query: string, options?: QueryOptions): Promise<QueryResult> { if (!this.pool) { throw new Error('Not connected to database.'); } const startTime = Date.now(); try { let result: MockQueryResult; if (options?.parameters && options.parameters.length > 0) { result = await this.pool.query(query, options.parameters); } else { result = await this.pool.query(query); } const executionTime = Date.now() - startTime; return { success: true, recordset: result.rows, data: result.rows, rowsAffected: result.rowCount || 0, executionTime, }; } catch (error) { const executionTime = Date.now() - startTime; return { success: false, error: error instanceof Error ? error.message : String(error), rowsAffected: 0, executionTime, }; } } /** * Get table list (returns TableInfo array) */ async getTables(): Promise<TableInfo[]> { const query = ` SELECT table_name as name, table_schema as tableSchema, table_type as type FROM information_schema.tables WHERE table_schema NOT IN ('information_schema', 'pg_catalog') AND table_type IN ('BASE TABLE', 'VIEW') ORDER BY table_schema, table_name `; const result = await this.executeQuery(query); if (!result.success || !result.recordset) { return []; } return result.recordset.map(row => ({ name: row.name, schema: row.tableSchema, type: row.type === 'BASE TABLE' ? ('table' as const) : ('view' as const), columns: [], primaryKeys: [], foreignKeys: [], indexes: [], })); } /** * Get table name list (returns string array) */ async getTableNames(): Promise<string[]> { const query = ` SELECT table_name as name FROM information_schema.tables WHERE table_schema NOT IN ('information_schema', 'pg_catalog') AND table_type IN ('BASE TABLE', 'VIEW') ORDER BY table_name `; const result = await this.executeQuery(query); if (!result.success || !result.recordset) { return []; } return result.recordset.map(row => row.name); } /** * Get table detailed information list (maintained for implementation compatibility) */ async getTableInfos(): Promise<TableInfo[]> { return this.getTables(); } /** * Get specific table information */ async getTableInfo(tableName: string): Promise<TableInfo> { // Parse schema.table format const [schema, table] = tableName.includes('.') ? tableName.split('.') : ['public', tableName]; // Get column information const columnsQuery = ` SELECT column_name as name, data_type as type, character_maximum_length as max_length, numeric_precision as precision, numeric_scale as scale, is_nullable as is_nullable, column_default as default_value, ordinal_position as ordinal_position FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 ORDER BY ordinal_position `; const columnsResult = await this.executeQuery(columnsQuery, { parameters: [schema, table], }); // Get primary key information const primaryKeysQuery = ` SELECT kcu.column_name as 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 primaryKeysResult = await this.executeQuery(primaryKeysQuery, { parameters: [schema, table], }); // Get foreign key information const foreignKeysQuery = ` SELECT kcu.column_name as name, ccu.table_name as referenced_table, ccu.column_name as referenced_column 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 JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1 AND tc.table_name = $2 `; const foreignKeysResult = await this.executeQuery(foreignKeysQuery, { parameters: [schema, table], }); // Get index information const indexesQuery = ` SELECT DISTINCT indexname as name FROM pg_indexes WHERE schemaname = $1 AND tablename = $2 AND indexname NOT LIKE '%_pkey' `; const indexesResult = await this.executeQuery(indexesQuery, { parameters: [schema, table], }); const primaryKeyNames = new Set(primaryKeysResult.recordset?.map(row => row.name) || []); const columns: ColumnInfo[] = columnsResult.recordset?.map(row => ({ name: row.name, dataType: row.type, maxLength: row.max_length, precision: row.precision, scale: row.scale, isNullable: row.is_nullable === 'YES', isIdentity: row.default_value?.includes('nextval') || false, isPrimaryKey: primaryKeyNames.has(row.name), defaultValue: row.default_value, ordinalPosition: row.ordinal_position, })) || []; return { name: tableName, schema: schema, type: 'table', columns, primaryKeys: primaryKeysResult.recordset?.map(row => row.name) || [], foreignKeys: foreignKeysResult.recordset || [], indexes: indexesResult.recordset || [], }; } /** * Get schema information */ async getSchema(includeDetailedInfo: boolean = true): Promise<SchemaInfo> { const tables = await this.getTables(); // If detailed info is requested, populate each table's detailed information if (includeDetailedInfo && tables.length > 0) { for (let i = 0; i < tables.length; i++) { try { const tableInfo = await this.getTableInfo(tables[i].name); tables[i].columns = tableInfo.columns; tables[i].primaryKeys = tableInfo.primaryKeys; tables[i].foreignKeys = tableInfo.foreignKeys; tables[i].indexes = tableInfo.indexes; } catch (error) { // Keep empty arrays for failed tables } } } return { name: this.config.database, tables, views: tables.filter(t => t.type === 'view'), functions: [], // PostgreSQL function query (separate implementation needed) procedures: [], // PostgreSQL procedure query (separate implementation needed) }; } /** * Test connection status */ async testConnection(): Promise<boolean> { try { if (!this.pool) return false; const result = await this.executeQuery('SELECT 1 as test'); return result.success && result.recordset?.[0]?.test === 1; } catch { return false; } } /** * Return database type */ getType(): DatabaseType { return 'postgresql'; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ryudg/mcp-sql'

If you have feedback or need assistance with the MCP directory API, please join our Discord server