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:
7
src/utils/index.ts
Normal file
7
src/utils/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* MCP Outline PostgreSQL - Utils Index
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
|
||||
export * from './logger.js';
|
||||
export * from './security.js';
|
||||
90
src/utils/logger.ts
Normal file
90
src/utils/logger.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* MCP Outline PostgreSQL - Logger
|
||||
* Optimized for MCP - reduce logs to not exhaust Claude context
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
|
||||
type LogLevel = 'error' | 'warn' | 'info' | 'debug';
|
||||
|
||||
interface LogEntry {
|
||||
timestamp: string;
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const LOG_LEVELS: Record<LogLevel, number> = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
debug: 3
|
||||
};
|
||||
|
||||
class Logger {
|
||||
private level: LogLevel;
|
||||
|
||||
constructor() {
|
||||
this.level = (process.env.LOG_LEVEL as LogLevel) || 'error';
|
||||
}
|
||||
|
||||
private shouldLog(level: LogLevel): boolean {
|
||||
return LOG_LEVELS[level] <= LOG_LEVELS[this.level];
|
||||
}
|
||||
|
||||
private formatLog(level: LogLevel, message: string, data?: Record<string, unknown>): string {
|
||||
const entry: LogEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
message,
|
||||
...(data && { data })
|
||||
};
|
||||
return JSON.stringify(entry);
|
||||
}
|
||||
|
||||
private write(level: LogLevel, message: string, data?: Record<string, unknown>): void {
|
||||
if (!this.shouldLog(level)) return;
|
||||
|
||||
const formatted = this.formatLog(level, message, data);
|
||||
|
||||
// For MCP, send logs to stderr
|
||||
if (process.env.MCP_MODE !== 'false') {
|
||||
process.stderr.write(formatted + '\n');
|
||||
} else {
|
||||
console.log(formatted);
|
||||
}
|
||||
}
|
||||
|
||||
error(message: string, data?: Record<string, unknown>): void {
|
||||
this.write('error', message, data);
|
||||
}
|
||||
|
||||
warn(message: string, data?: Record<string, unknown>): void {
|
||||
this.write('warn', message, data);
|
||||
}
|
||||
|
||||
info(message: string, data?: Record<string, unknown>): void {
|
||||
this.write('info', message, data);
|
||||
}
|
||||
|
||||
debug(message: string, data?: Record<string, unknown>): void {
|
||||
this.write('debug', message, data);
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = new Logger();
|
||||
|
||||
// Log queries for auditing (if enabled) - OPTIMIZED
|
||||
export function logQuery(
|
||||
sql: string,
|
||||
_params?: any[],
|
||||
duration?: number,
|
||||
_clientId?: string
|
||||
): void {
|
||||
// DISABLED by default to save Claude context
|
||||
if (process.env.ENABLE_AUDIT_LOG === 'true' && process.env.NODE_ENV !== 'production') {
|
||||
logger.debug('SQL', {
|
||||
sql: sql.substring(0, 50),
|
||||
duration
|
||||
});
|
||||
}
|
||||
}
|
||||
115
src/utils/security.ts
Normal file
115
src/utils/security.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* MCP Outline PostgreSQL - Security Utilities
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
|
||||
// Rate limiting store
|
||||
const rateLimitStore: Map<string, { count: number; resetAt: number }> = new Map();
|
||||
|
||||
// Rate limit configuration
|
||||
const RATE_LIMIT_WINDOW = 60000; // 1 minute
|
||||
const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX || '100', 10);
|
||||
|
||||
/**
|
||||
* Check if a request should be rate limited
|
||||
*/
|
||||
export function checkRateLimit(type: string, clientId: string): boolean {
|
||||
const key = `${type}:${clientId}`;
|
||||
const now = Date.now();
|
||||
const entry = rateLimitStore.get(key);
|
||||
|
||||
if (!entry || now > entry.resetAt) {
|
||||
rateLimitStore.set(key, { count: 1, resetAt: now + RATE_LIMIT_WINDOW });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entry.count >= RATE_LIMIT_MAX) {
|
||||
return false;
|
||||
}
|
||||
|
||||
entry.count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize SQL input to prevent injection
|
||||
* Note: Always use parameterized queries, this is a secondary safety measure
|
||||
*/
|
||||
export function sanitizeInput(input: string): string {
|
||||
if (typeof input !== 'string') return input;
|
||||
|
||||
// Remove null bytes
|
||||
let sanitized = input.replace(/\0/g, '');
|
||||
|
||||
// Trim whitespace
|
||||
sanitized = sanitized.trim();
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate UUID format
|
||||
*/
|
||||
export function isValidUUID(uuid: string): boolean {
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate URL ID format (Outline uses URL-safe IDs)
|
||||
*/
|
||||
export function isValidUrlId(urlId: string): boolean {
|
||||
const urlIdRegex = /^[a-zA-Z0-9_-]+$/;
|
||||
return urlIdRegex.test(urlId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate email format
|
||||
*/
|
||||
export function isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML entities for safe display
|
||||
*/
|
||||
export function escapeHtml(text: string): string {
|
||||
const map: Record<string, string> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, (char) => map[char]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate pagination parameters
|
||||
*/
|
||||
export function validatePagination(limit?: number, offset?: number): { limit: number; offset: number } {
|
||||
const maxLimit = 100;
|
||||
const defaultLimit = 25;
|
||||
|
||||
return {
|
||||
limit: Math.min(Math.max(1, limit || defaultLimit), maxLimit),
|
||||
offset: Math.max(0, offset || 0)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate sort direction
|
||||
*/
|
||||
export function validateSortDirection(direction?: string): 'ASC' | 'DESC' {
|
||||
const upper = (direction || 'DESC').toUpperCase();
|
||||
return upper === 'ASC' ? 'ASC' : 'DESC';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate sort field against allowed fields
|
||||
*/
|
||||
export function validateSortField(field: string | undefined, allowedFields: string[], defaultField: string): string {
|
||||
if (!field) return defaultField;
|
||||
return allowedFields.includes(field) ? field : defaultField;
|
||||
}
|
||||
Reference in New Issue
Block a user