Files
mcp-outline-postgresql/docs/audits/2026-01-31-v1.2.1/PLANO-MELHORIAS.md
Emanuel Almeida 7c83a9e168 fix(security): Resolve 21 SQL injection vulnerabilities and add transactions
Security fixes (v1.2.2):
- Fix SQL injection in analytics.ts (16 occurrences)
- Fix SQL injection in advanced-search.ts (1 occurrence)
- Fix SQL injection in search-queries.ts (1 occurrence)
- Add validateDaysInterval(), isValidISODate(), validatePeriod() to security.ts
- Use make_interval(days => N) for safe PostgreSQL intervals
- Validate UUIDs BEFORE string construction

Transaction support:
- bulk-operations.ts: 6 atomic operations with withTransaction()
- desk-sync.ts: 2 operations with transactions
- export-import.ts: 1 operation with transaction

Rate limiting:
- Add automatic cleanup of expired entries (every 5 minutes)

Audit:
- Archive previous audit docs to docs/audits/2026-01-31-v1.2.1/
- Create new AUDIT-REQUEST.md for v1.2.2 verification

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 14:47:41 +00:00

27 KiB

Plano de Melhorias - MCP Outline PostgreSQL v1.2.1

Data: 2026-01-31
Projecto: MCP Outline PostgreSQL
Versão Actual: 1.2.1
Versão Alvo: 2.0.0 (Production-Ready)


🎯 Objectivo

Transformar o MCP Outline PostgreSQL de um protótipo funcional (7.2/10) num servidor production-ready (9.5/10) através de correcções de segurança críticas, optimizações de performance e melhorias de qualidade.


📋 Resumo das Melhorias

Fase Foco Duração Prioridade Tarefas
Fase 1 Segurança Crítica 2 semanas 🔴 P0 12
Fase 2 Performance 1 semana 🟡 P1 10
Fase 3 Qualidade 2 semanas 🟢 P2 15
Fase 4 Funcionalidades Ongoing 🟢 P3 15

Total: 52 tarefas | 5 semanas de trabalho core


🔴 FASE 1: Segurança Crítica (P0)

Duração: 2 semanas
Prioridade: CRÍTICA
Bloqueante: SIM - Não deploy sem esta fase completa

Objectivos

  • Eliminar 100% das vulnerabilidades de SQL Injection
  • Implementar transacções em todas as operações críticas
  • Adicionar validação robusta de inputs
  • Implementar audit logging básico

Tarefas Detalhadas

1.1 Corrigir SQL Injection (Semana 1)

Problema: 164 tools com potencial SQL injection via string concatenation.

Solução: Converter todas as queries para prepared statements parametrizados.

1.1.1 Auditar e Catalogar Queries Vulneráveis
# Script para identificar queries vulneráveis
grep -r "pool.query(\`" src/tools/ > vulnerable-queries.txt

Ficheiros Prioritários:

  • documents.ts (19 tools) - 2 dias
  • collections.ts (14 tools) - 1.5 dias
  • users.ts (9 tools) - 1 dia
  • advanced-search.ts (6 tools) - 1 dia
  • analytics.ts (6 tools) - 1 dia
  • Restantes 27 ficheiros - 2 dias

Total: 8.5 dias

1.1.2 Criar Função Helper para Queries Seguras

Ficheiro: src/utils/query-builder.ts

/**
 * Query builder seguro com prepared statements
 */
export class SafeQueryBuilder {
  private params: any[] = [];
  private paramIndex = 1;

  /**
   * Adiciona parâmetro e retorna placeholder
   */
  addParam(value: any): string {
    this.params.push(value);
    return `$${this.paramIndex++}`;
  }

  /**
   * Constrói WHERE clause com ILIKE seguro
   */
  buildILike(column: string, value: string): string {
    return `${column} ILIKE ${this.addParam(`%${sanitizeInput(value)}%`)}`;
  }

  /**
   * Constrói IN clause seguro
   */
  buildIn(column: string, values: any[]): string {
    const placeholders = values.map(v => this.addParam(v)).join(', ');
    return `${column} IN (${placeholders})`;
  }

  getParams(): any[] {
    return this.params;
  }
}

Estimativa: 0.5 dias

1.1.3 Refactoring de Queries

Antes (VULNERÁVEL):

const query = `
  SELECT * FROM documents 
  WHERE title ILIKE '%${args.query}%'
  AND collectionId = '${args.collection_id}'
`;
const result = await pool.query(query);

Depois (SEGURO):

const qb = new SafeQueryBuilder();
const query = `
  SELECT * FROM documents 
  WHERE title ILIKE ${qb.addParam(`%${sanitizeInput(args.query)}%`)}
  AND collectionId = ${qb.addParam(args.collection_id)}
`;
const result = await pool.query(query, qb.getParams());

Estimativa: 8 dias (todos os ficheiros)

1.1.4 Adicionar Linting Rule

Ficheiro: .eslintrc.json

{
  "rules": {
    "no-template-curly-in-string": "error",
    "no-restricted-syntax": [
      "error",
      {
        "selector": "TemplateLiteral[parent.callee.property.name='query']",
        "message": "Use parameterized queries to prevent SQL injection"
      }
    ]
  }
}

Estimativa: 0.5 dias


1.2 Implementar Transacções (Semana 2)

Problema: Operações multi-write sem atomicidade.

Solução: Envolver operações críticas em transacções.

1.2.1 Identificar Operações Críticas

Ficheiros Afectados:

  • bulk-operations.ts - 6 tools
  • desk-sync.ts - 2 tools
  • export-import.ts - 2 tools
  • collections.ts - memberships (4 tools)
  • documents.ts - create/update com memberships (2 tools)

Total: 16 tools

1.2.2 Criar Transaction Helper

Ficheiro: src/utils/transaction.ts

import { Pool, PoolClient } from 'pg';

/**
 * Executa operação em transacção com retry automático
 */
export async function withTransaction<T>(
  pool: Pool,
  callback: (client: PoolClient) => Promise<T>,
  maxRetries = 3
): Promise<T> {
  let lastError: Error | null = null;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    const client = await pool.connect();
    try {
      await client.query('BEGIN');
      const result = await callback(client);
      await client.query('COMMIT');
      return result;
    } catch (error) {
      await client.query('ROLLBACK');
      lastError = error as Error;
      
      // Retry apenas em deadlocks
      if (error instanceof Error && error.message.includes('deadlock')) {
        await new Promise(resolve => setTimeout(resolve, 100 * attempt));
        continue;
      }
      throw error;
    } finally {
      client.release();
    }
  }

  throw lastError!;
}

Estimativa: 0.5 dias

1.2.3 Refactoring de Operações Bulk

Antes (SEM TRANSACÇÃO):

handler: async (args, pgClient) => {
  const pool = pgClient.getPool();
  for (const id of args.document_ids) {
    await pool.query('UPDATE documents SET archivedAt = NOW() WHERE id = $1', [id]);
  }
}

Depois (COM TRANSACÇÃO):

handler: async (args, pgClient) => {
  const pool = pgClient.getPool();
  
  return await withTransaction(pool, async (client) => {
    const results = [];
    for (const id of args.document_ids) {
      const result = await client.query(
        'UPDATE documents SET archivedAt = NOW() WHERE id = $1 RETURNING *',
        [id]
      );
      results.push(result.rows[0]);
    }
    return results;
  });
}

Estimativa: 2 dias (16 tools)

1.2.4 Testes de Rollback

Ficheiro: tests/transactions.test.ts

describe('Transaction Rollback', () => {
  it('should rollback on error in bulk operations', async () => {
    const invalidIds = ['valid-uuid', 'INVALID'];
    
    await expect(
      bulkArchiveDocuments({ document_ids: invalidIds }, pgClient)
    ).rejects.toThrow();
    
    // Verificar que nenhum documento foi arquivado
    const result = await pool.query(
      'SELECT * FROM documents WHERE archivedAt IS NOT NULL'
    );
    expect(result.rows).toHaveLength(0);
  });
});

Estimativa: 1 dia


1.3 Validação de Inputs (Semana 2)

1.3.1 Implementar Validação Automática

Ficheiro: src/utils/validation.ts

import Ajv from 'ajv';
import addFormats from 'ajv-formats';

const ajv = new Ajv({ allErrors: true });
addFormats(ajv);

/**
 * Valida args contra inputSchema
 */
export function validateToolInput<T>(
  args: unknown,
  schema: object
): T {
  const validate = ajv.compile(schema);
  
  if (!validate(args)) {
    const errors = validate.errors?.map(e => `${e.instancePath} ${e.message}`).join(', ');
    throw new Error(`Invalid input: ${errors}`);
  }
  
  return args as T;
}

/**
 * Middleware para validação automática
 */
export function withValidation<T>(
  tool: BaseTool<T>
): BaseTool<T> {
  const originalHandler = tool.handler;
  
  return {
    ...tool,
    handler: async (args, pgClient) => {
      const validatedArgs = validateToolInput<T>(args, tool.inputSchema);
      return originalHandler(validatedArgs, pgClient);
    }
  };
}

Estimativa: 1 dia

1.3.2 Adicionar Validações Específicas

Melhorias em src/utils/security.ts:

/**
 * Valida array de UUIDs
 */
export function validateUUIDs(uuids: string[]): void {
  const invalid = uuids.filter(uuid => !isValidUUID(uuid));
  if (invalid.length > 0) {
    throw new Error(`Invalid UUIDs: ${invalid.join(', ')}`);
  }
}

/**
 * Valida enum value
 */
export function validateEnum<T>(
  value: string,
  allowedValues: T[],
  fieldName: string
): T {
  if (!allowedValues.includes(value as T)) {
    throw new Error(
      `Invalid ${fieldName}: ${value}. Allowed: ${allowedValues.join(', ')}`
    );
  }
  return value as T;
}

/**
 * Valida tamanho de string
 */
export function validateStringLength(
  value: string,
  min: number,
  max: number,
  fieldName: string
): void {
  if (value.length < min || value.length > max) {
    throw new Error(
      `${fieldName} must be between ${min} and ${max} characters`
    );
  }
}

Estimativa: 0.5 dias


1.4 Audit Logging Básico (Semana 2)

1.4.1 Criar Sistema de Audit Log

Ficheiro: src/utils/audit.ts

import { Pool } from 'pg';

export interface AuditLogEntry {
  userId?: string;
  action: string;
  resourceType: string;
  resourceId: string;
  metadata?: Record<string, any>;
}

/**
 * Regista operação em audit log
 */
export async function logAudit(
  pool: Pool,
  entry: AuditLogEntry
): Promise<void> {
  await pool.query(
    `INSERT INTO events (name, actorId, modelId, data, createdAt)
     VALUES ($1, $2, $3, $4, NOW())`,
    [
      entry.action,
      entry.userId || null,
      entry.resourceId,
      JSON.stringify({
        resourceType: entry.resourceType,
        ...entry.metadata
      })
    ]
  );
}

/**
 * Middleware para audit logging automático
 */
export function withAuditLog<T>(
  tool: BaseTool<T>,
  getResourceInfo: (args: T) => { type: string; id: string }
): BaseTool<T> {
  const originalHandler = tool.handler;
  
  return {
    ...tool,
    handler: async (args, pgClient) => {
      const result = await originalHandler(args, pgClient);
      
      // Log apenas operações de escrita
      if (['create', 'update', 'delete'].some(op => tool.name.includes(op))) {
        const resource = getResourceInfo(args);
        await logAudit(pgClient.getPool(), {
          action: tool.name,
          resourceType: resource.type,
          resourceId: resource.id
        });
      }
      
      return result;
    }
  };
}

Estimativa: 1 dia


Checklist Fase 1

  • SQL Injection

    • Auditar 164 tools
    • Criar SafeQueryBuilder
    • Refactoring completo
    • Adicionar linting rule
    • Testar manualmente cada módulo
  • Transacções

    • Identificar operações críticas
    • Criar transaction helper
    • Refactoring bulk operations
    • Implementar testes de rollback
  • Validação

    • Implementar validação automática
    • Adicionar validações específicas
    • Aplicar a todos os tools
  • Audit Log

    • Criar sistema de logging
    • Integrar com tools de escrita
    • Testar logging

Métricas de Sucesso Fase 1

  • 0 queries com string concatenation
  • 100% queries parametrizadas
  • 16 operações críticas com transacções
  • 100% tools com validação de input
  • Audit log funcional em operações de escrita

🟡 FASE 2: Performance (P1)

Duração: 1 semana
Prioridade: ALTA
Bloqueante: NÃO - Mas recomendado antes de produção

Objectivos

  • Eliminar N+1 queries
  • Criar índices necessários
  • Optimizar connection pool
  • Implementar cursor-based pagination

Tarefas Detalhadas

2.1 Eliminar N+1 Queries (2 dias)

2.1.1 Identificar N+1 Queries

Ficheiros Afectados:

  • collections.ts:1253-1280 - export_all_collections
  • documents.ts:530-577 - list_drafts
  • analytics.ts - várias queries
2.1.2 Refactoring com JOINs

Antes (N+1):

const collections = await pool.query('SELECT * FROM collections');
for (const collection of collections.rows) {
  const docs = await pool.query(
    'SELECT * FROM documents WHERE collectionId = $1',
    [collection.id]
  );
  collection.documents = docs.rows;
}

Depois (JOIN):

const result = await pool.query(`
  SELECT 
    c.*,
    json_agg(
      json_build_object(
        'id', d.id,
        'title', d.title,
        'createdAt', d.createdAt
      )
    ) FILTER (WHERE d.id IS NOT NULL) as documents
  FROM collections c
  LEFT JOIN documents d ON d.collectionId = c.id AND d.deletedAt IS NULL
  GROUP BY c.id
`);

Estimativa: 2 dias


2.2 Criar Índices (1 dia)

2.2.1 Documentar Índices Necessários

Ficheiro: migrations/001_indexes.sql

-- Full-text search
CREATE INDEX IF NOT EXISTS idx_documents_search 
ON documents USING gin(to_tsvector('english', title || ' ' || COALESCE(text, '')));

-- Queries comuns
CREATE INDEX IF NOT EXISTS idx_documents_collection_id 
ON documents(collectionId) WHERE deletedAt IS NULL;

CREATE INDEX IF NOT EXISTS idx_documents_published 
ON documents(publishedAt DESC) WHERE deletedAt IS NULL AND publishedAt IS NOT NULL;

CREATE INDEX IF NOT EXISTS idx_documents_created 
ON documents(createdAt DESC) WHERE deletedAt IS NULL;

-- Memberships
CREATE INDEX IF NOT EXISTS idx_collection_memberships_lookup 
ON collection_memberships(collectionId, userId);

CREATE INDEX IF NOT EXISTS idx_group_memberships_lookup 
ON group_memberships(groupId, userId);

-- Stars, Pins, Views
CREATE INDEX IF NOT EXISTS idx_stars_user_document 
ON stars(userId, documentId) WHERE deletedAt IS NULL;

CREATE INDEX IF NOT EXISTS idx_pins_collection_document 
ON pins(collectionId, documentId) WHERE deletedAt IS NULL;

CREATE INDEX IF NOT EXISTS idx_views_document_user 
ON views(documentId, userId, createdAt DESC);

-- Events (audit log)
CREATE INDEX IF NOT EXISTS idx_events_actor_created 
ON events(actorId, createdAt DESC);

CREATE INDEX IF NOT EXISTS idx_events_model_created 
ON events(modelId, createdAt DESC);

Estimativa: 0.5 dias

2.2.2 Documentar em SPEC

Adicionar secção em SPEC-MCP-OUTLINE.md:

## Índices Recomendados

Para performance optimal, execute as migrations em `migrations/001_indexes.sql`.

### Índices Críticos

| Índice | Tabela | Tipo | Impacto |
|--------|--------|------|---------|
| idx_documents_search | documents | GIN | Full-text search 10-100x faster |
| idx_documents_collection_id | documents | B-tree | List documents 5-10x faster |
| idx_collection_memberships_lookup | collection_memberships | B-tree | Permission checks 10x faster |

Estimativa: 0.5 dias


2.3 Optimizar Connection Pool (1 dia)

2.3.1 Tuning de Pool

Ficheiro: src/config/database.ts

export interface DatabaseConfig {
  // ... existing fields
  
  // Pool tuning
  max?: number;              // Default: 20
  min?: number;              // Default: 5
  idleTimeoutMillis?: number;  // Default: 30000
  connectionTimeoutMillis?: number;  // Default: 5000
  maxUses?: number;          // Default: 7500 (recycle connections)
  
  // Performance
  statementTimeout?: number;  // Default: 30000 (30s)
  queryTimeout?: number;      // Default: 10000 (10s)
}

export function getDefaultConfig(): DatabaseConfig {
  return {
    max: parseInt(process.env.DB_POOL_MAX || '20', 10),
    min: parseInt(process.env.DB_POOL_MIN || '5', 10),
    idleTimeoutMillis: 30000,
    connectionTimeoutMillis: 5000,
    maxUses: 7500,
    statementTimeout: 30000,
    queryTimeout: 10000
  };
}

Estimativa: 0.5 dias

2.3.2 Adicionar Pool Monitoring

Ficheiro: src/utils/monitoring.ts

import { Pool } from 'pg';

export function monitorPool(pool: Pool): void {
  setInterval(() => {
    logger.info('Pool stats', {
      total: pool.totalCount,
      idle: pool.idleCount,
      waiting: pool.waitingCount
    });
    
    // Alert se pool saturado
    if (pool.waitingCount > 5) {
      logger.warn('Pool saturation detected', {
        waiting: pool.waitingCount,
        total: pool.totalCount
      });
    }
  }, 60000); // A cada minuto
}

Estimativa: 0.5 dias


2.4 Cursor-Based Pagination (1 dia)

2.4.1 Implementar Cursor Pagination

Ficheiro: src/utils/pagination.ts

export interface CursorPaginationArgs {
  limit?: number;
  cursor?: string;  // Base64 encoded timestamp ou ID
}

export interface CursorPaginationResult<T> {
  items: T[];
  nextCursor?: string;
  hasMore: boolean;
}

/**
 * Cursor-based pagination (mais eficiente que OFFSET)
 */
export async function paginateWithCursor<T>(
  pool: Pool,
  baseQuery: string,
  cursorField: string,
  args: CursorPaginationArgs
): Promise<CursorPaginationResult<T>> {
  const limit = Math.min(args.limit || 25, 100);
  
  let query = baseQuery;
  const params: any[] = [limit + 1]; // +1 para detectar hasMore
  
  if (args.cursor) {
    const cursorValue = Buffer.from(args.cursor, 'base64').toString();
    query += ` AND ${cursorField} < $2`;
    params.push(cursorValue);
  }
  
  query += ` ORDER BY ${cursorField} DESC LIMIT $1`;
  
  const result = await pool.query<T>(query, params);
  const hasMore = result.rows.length > limit;
  const items = hasMore ? result.rows.slice(0, -1) : result.rows;
  
  const nextCursor = hasMore && items.length > 0
    ? Buffer.from(String((items[items.length - 1] as any)[cursorField])).toString('base64')
    : undefined;
  
  return { items, nextCursor, hasMore };
}

Estimativa: 1 dia


Checklist Fase 2

  • N+1 Queries

    • Identificar todas as ocorrências
    • Refactoring com JOINs
    • Testar performance
  • Índices

    • Criar migrations/001_indexes.sql
    • Documentar em SPEC
    • Testar impacto
  • Connection Pool

    • Tuning de configuração
    • Adicionar monitoring
    • Testar sob carga
  • Pagination

    • Implementar cursor-based
    • Migrar tools principais
    • Benchmark vs OFFSET

Métricas de Sucesso Fase 2

  • 0 N+1 queries em hot paths
  • Queries < 100ms (p95)
  • Índices criados e documentados
  • Pool utilization < 80%
  • Cursor pagination em listagens principais

🟢 FASE 3: Qualidade (P2)

Duração: 2 semanas
Prioridade: MÉDIA
Bloqueante: NÃO - Melhoria contínua

Objectivos

  • Implementar testes unitários
  • Adicionar testes de integração
  • Configurar CI/CD
  • Refactoring de código duplicado
  • Melhorar documentação

Tarefas Detalhadas

3.1 Testes Unitários (1 semana)

3.1.1 Setup de Testing

Ficheiro: package.json

{
  "devDependencies": {
    "vitest": "^1.0.0",
    "@vitest/coverage-v8": "^1.0.0",
    "testcontainers": "^10.0.0"
  },
  "scripts": {
    "test": "vitest",
    "test:coverage": "vitest --coverage",
    "test:ui": "vitest --ui"
  }
}

Ficheiro: vitest.config.ts

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: ['dist/', 'tests/', '**/*.test.ts']
    }
  }
});

Estimativa: 0.5 dias

3.1.2 Testes de Utils

Ficheiro: tests/utils/security.test.ts

import { describe, it, expect } from 'vitest';
import {
  isValidUUID,
  isValidEmail,
  sanitizeInput,
  validatePagination
} from '../../src/utils/security';

describe('Security Utils', () => {
  describe('isValidUUID', () => {
    it('should validate correct UUIDs', () => {
      expect(isValidUUID('123e4567-e89b-12d3-a456-426614174000')).toBe(true);
    });
    
    it('should reject invalid UUIDs', () => {
      expect(isValidUUID('not-a-uuid')).toBe(false);
      expect(isValidUUID('')).toBe(false);
    });
  });
  
  describe('sanitizeInput', () => {
    it('should remove null bytes', () => {
      expect(sanitizeInput('test\0data')).toBe('testdata');
    });
    
    it('should trim whitespace', () => {
      expect(sanitizeInput('  test  ')).toBe('test');
    });
  });
  
  describe('validatePagination', () => {
    it('should enforce max limit', () => {
      const result = validatePagination(1000, 0);
      expect(result.limit).toBe(100);
    });
    
    it('should use defaults', () => {
      const result = validatePagination();
      expect(result.limit).toBe(25);
      expect(result.offset).toBe(0);
    });
  });
});

Estimativa: 2 dias (todos os utils)

3.1.3 Testes de Tools

Ficheiro: tests/tools/documents.test.ts

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { GenericContainer, StartedTestContainer } from 'testcontainers';
import { Pool } from 'pg';
import { documentsTools } from '../../src/tools/documents';

describe('Documents Tools', () => {
  let container: StartedTestContainer;
  let pool: Pool;
  
  beforeAll(async () => {
    // Start PostgreSQL container
    container = await new GenericContainer('postgres:15')
      .withEnvironment({ POSTGRES_PASSWORD: 'test' })
      .withExposedPorts(5432)
      .start();
    
    pool = new Pool({
      host: container.getHost(),
      port: container.getMappedPort(5432),
      user: 'postgres',
      password: 'test',
      database: 'postgres'
    });
    
    // Setup schema
    await pool.query(/* schema SQL */);
  });
  
  afterAll(async () => {
    await pool.end();
    await container.stop();
  });
  
  it('should list documents', async () => {
    const result = await documentsTools[0].handler({}, { getPool: () => pool });
    expect(result.content[0].text).toBeDefined();
  });
});

Estimativa: 3 dias (tools principais)


3.2 CI/CD (2 dias)

3.2.1 GitHub Actions

Ficheiro: .github/workflows/ci.yml

name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - run: npm ci
      - run: npm run build
      - run: npm run test:coverage
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
  
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run lint

Estimativa: 1 dia


3.3 Refactoring (3 dias)

3.3.1 Eliminar Duplicação

Criar: src/utils/tool-factory.ts

/**
 * Factory para criar tools com padrão consistente
 */
export function createTool<T>(config: {
  name: string;
  description: string;
  inputSchema: object;
  handler: (args: T, pool: Pool) => Promise<any>;
  requiresTransaction?: boolean;
  auditLog?: boolean;
}): BaseTool<T> {
  return {
    name: config.name,
    description: config.description,
    inputSchema: config.inputSchema,
    handler: async (args, pgClient) => {
      try {
        // Validação automática
        const validatedArgs = validateToolInput<T>(args, config.inputSchema);
        
        const pool = pgClient.getPool();
        
        // Executar com ou sem transacção
        const result = config.requiresTransaction
          ? await withTransaction(pool, client => config.handler(validatedArgs, client))
          : await config.handler(validatedArgs, pool);
        
        // Audit log automático
        if (config.auditLog) {
          await logAudit(pool, {
            action: config.name,
            resourceType: extractResourceType(config.name),
            resourceId: extractResourceId(result)
          });
        }
        
        return {
          content: [{
            type: 'text',
            text: JSON.stringify(result, null, 2)
          }]
        };
      } catch (error) {
        logger.error(`Tool ${config.name} failed`, { error });
        throw error;
      }
    }
  };
}

Estimativa: 2 dias

3.3.2 Aplicar Factory

Refactoring de tools para usar factory:

// Antes
const listDocuments: BaseTool<DocumentArgs> = {
  name: 'list_documents',
  description: '...',
  inputSchema: { /* ... */ },
  handler: async (args, pgClient) => {
    try {
      const pool = pgClient.getPool();
      // ... lógica
      return { content: [{ type: 'text', text: JSON.stringify(result) }] };
    } catch (error) {
      // ...
    }
  }
};

// Depois
const listDocuments = createTool<DocumentArgs>({
  name: 'list_documents',
  description: '...',
  inputSchema: { /* ... */ },
  handler: async (args, pool) => {
    // ... apenas lógica de negócio
    return result;
  }
});

Estimativa: 1 dia


Checklist Fase 3

  • Testes

    • Setup Vitest + Testcontainers
    • Testes de utils (100% coverage)
    • Testes de tools principais (>80% coverage)
    • Testes de integração
  • CI/CD

    • GitHub Actions
    • Codecov integration
    • Automated releases
  • Refactoring

    • Tool factory
    • Eliminar duplicação
    • Melhorar type safety

Métricas de Sucesso Fase 3

  • Code coverage > 80%
  • 0 linting errors
  • CI passing
  • Duplicação < 5%

🟢 FASE 4: Funcionalidades (P3)

Duração: Ongoing
Prioridade: BAIXA
Bloqueante: NÃO - Melhorias incrementais

Tarefas

4.1 Rate Limiting Distribuído

  • Integrar Redis
  • Implementar rate limiting distribuído
  • Adicionar CAPTCHA para operações sensíveis

4.2 Autorização

  • Implementar RBAC
  • Verificar permissões antes de operações
  • Adicionar testes de autorização

4.3 Monitoring

  • Integrar Prometheus
  • Adicionar métricas de performance
  • Dashboard Grafana

4.4 Documentação

  • API documentation (OpenAPI)
  • Guia de deployment
  • Troubleshooting guide

📊 Métricas Globais de Sucesso

Segurança

  • 0 vulnerabilidades críticas
  • 0 vulnerabilidades altas
  • 100% queries parametrizadas
  • 100% operações críticas com transacções

Performance

  • Queries < 100ms (p95)
  • Throughput > 1000 req/s
  • Pool utilization < 80%
  • 0 N+1 queries

Qualidade

  • Code coverage > 80%
  • 0 linting errors
  • CI passing
  • Duplicação < 5%

📅 Timeline

Semana 1: SQL Injection
Semana 2: Transacções + Validação + Audit
Semana 3: Performance (N+1, Índices, Pool)
Semana 4: Testes Unitários
Semana 5: CI/CD + Refactoring
Semana 6+: Funcionalidades (ongoing)

🎯 Próximos Passos Imediatos

  1. Aprovar este plano
  2. Criar branch security-fixes
  3. Iniciar Fase 1.1.1: Auditar queries vulneráveis
  4. Daily progress tracking em task.md

Plano criado em 2026-01-31 | MCP Outline PostgreSQL v1.2.1 → v2.0.0