feat: Initial release MCP Outline PostgreSQL v1.0.0

86 tools across 12 modules for direct PostgreSQL access to Outline Wiki:
- Documents (19), Collections (14), Users (9), Groups (8)
- Comments (6), Shares (5), Revisions (3), Events (3)
- Attachments (5), File Operations (4), OAuth (8), Auth (2)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 13:25:09 +00:00
commit b05b54033f
30 changed files with 14439 additions and 0 deletions

158
src/pg-client.ts Normal file
View File

@@ -0,0 +1,158 @@
/**
* MCP Outline PostgreSQL - PostgreSQL Client
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
*/
import { Pool, PoolConfig, QueryResult, QueryResultRow } from 'pg';
import { DatabaseConfig } from './config/database.js';
import { logger } from './utils/logger.js';
export class PgClient {
private pool: Pool;
private isConnected: boolean = false;
constructor(config: DatabaseConfig) {
const poolConfig: PoolConfig = config.connectionString
? {
connectionString: config.connectionString,
max: config.max,
idleTimeoutMillis: config.idleTimeoutMillis,
connectionTimeoutMillis: config.connectionTimeoutMillis
}
: {
host: config.host,
port: config.port,
user: config.user,
password: config.password,
database: config.database,
ssl: config.ssl ? { rejectUnauthorized: false } : false,
max: config.max,
idleTimeoutMillis: config.idleTimeoutMillis,
connectionTimeoutMillis: config.connectionTimeoutMillis
};
this.pool = new Pool(poolConfig);
// Handle pool errors
this.pool.on('error', (err) => {
logger.error('Unexpected PostgreSQL pool error', { error: err.message });
});
}
/**
* Get the underlying pool for direct access
*/
getPool(): Pool {
return this.pool;
}
/**
* Test database connection
*/
async testConnection(): Promise<boolean> {
try {
const client = await this.pool.connect();
await client.query('SELECT 1');
client.release();
this.isConnected = true;
logger.info('PostgreSQL connection successful');
return true;
} catch (error) {
logger.error('PostgreSQL connection failed', {
error: error instanceof Error ? error.message : String(error)
});
this.isConnected = false;
return false;
}
}
/**
* Execute a query with parameters
*/
async query<T extends QueryResultRow = any>(sql: string, params?: any[]): Promise<T[]> {
const start = Date.now();
try {
const result = await this.pool.query<T>(sql, params);
const duration = Date.now() - start;
logger.debug('Query executed', {
sql: sql.substring(0, 100),
duration,
rowCount: result.rowCount
});
return result.rows;
} catch (error) {
const duration = Date.now() - start;
logger.error('Query failed', {
sql: sql.substring(0, 100),
duration,
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
/**
* Execute a query and return the full result
*/
async queryRaw<T extends QueryResultRow = any>(sql: string, params?: any[]): Promise<QueryResult<T>> {
return this.pool.query<T>(sql, params);
}
/**
* Execute a query and return a single row
*/
async queryOne<T extends QueryResultRow = any>(sql: string, params?: any[]): Promise<T | null> {
const rows = await this.query<T>(sql, params);
return rows.length > 0 ? rows[0] : null;
}
/**
* Execute multiple queries in a transaction
*/
async transaction<T>(callback: (client: any) => Promise<T>): Promise<T> {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
const result = await callback(client);
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
/**
* Close the pool
*/
async close(): Promise<void> {
await this.pool.end();
this.isConnected = false;
logger.info('PostgreSQL pool closed');
}
/**
* Check if connected
*/
isPoolConnected(): boolean {
return this.isConnected;
}
}
// Export a singleton factory
let instance: PgClient | null = null;
export function createPgClient(config: DatabaseConfig): PgClient {
if (!instance) {
instance = new PgClient(config);
}
return instance;
}
export function getPgClient(): PgClient | null {
return instance;
}