Guias

MCP QA Agent - Guia Completo de Implementação

MCP QA Agent - Guia Completo de Implementação

Visão Geral

Este documento descreve a implementação de um MCP (Model Context Protocol) Server que permite ao Claude Code executar testes automatizados de frontend usando Playwright, capturando logs do console, erros de rede e screenshots para validar se as features implementadas estão funcionando corretamente.

Arquitetura

┌─────────────────────────────────────────────────────────────────┐
│                        Claude Code                               │
│                             │                                    │
│                             ▼                                    │
│                    ┌────────────────┐                           │
│                    │   MCP Server   │                           │
│                    │   (qa-agent)   │                           │
│                    └───────┬────────┘                           │
│                            │                                     │
│         ┌──────────────────┼──────────────────┐                 │
│         ▼                  ▼                  ▼                 │
│  ┌─────────────┐   ┌─────────────┐   ┌─────────────┐           │
│  │   Context   │   │  Playwright │   │    Flow     │           │
│  │   Loader    │   │   Runner    │   │   Manager   │           │
│  │ (docs/*.md) │   │  (browser)  │   │   (yaml)    │           │
│  └─────────────┘   └─────────────┘   └─────────────┘           │
│                            │                                     │
│                            ▼                                     │
│                    ┌─────────────┐                              │
│                    │  Resultados │                              │
│                    │  + Análise  │                              │
│                    └─────────────┘                              │
└─────────────────────────────────────────────────────────────────┘

Funcionalidades

  • Carregamento de contexto: Lê documentação do projeto (docs/*.md) para entender a estrutura
  • Execução de fluxos: Roda testes definidos em YAML no navegador real
  • Captura completa: Console logs, erros JS, requisições de rede, screenshots
  • Análise automática: Identifica problemas e sugere correções
  • Integração Claude Code: Ferramentas disponíveis diretamente no chat

Pré-requisitos

Sistema

  • Node.js 18+
  • npm ou yarn
  • Chromium (instalado pelo Playwright)

Conhecimento do Projeto

  • Estrutura de pastas do monorepo
  • URL base do frontend em desenvolvimento
  • Fluxos principais da aplicação

Estrutura do Projeto

Localização Recomendada

seu-monorepo/
├── apps/
│   └── frontend/
├── packages/
├── docs/                    # Documentação existente do projeto
│   ├── arquitetura.md
│   ├── fluxos.md
│   └── ...
└── tools/
    └── mcp-qa-agent/        # ← MCP Server aqui
        ├── src/
        │   ├── index.ts
        │   ├── server.ts
        │   ├── tools/
        │   │   ├── index.ts
        │   │   ├── load-context.ts
        │   │   ├── validate-page.ts
        │   │   ├── run-flow.ts
        │   │   └── manage-flows.ts
        │   ├── playwright/
        │   │   ├── browser-manager.ts
        │   │   └── page-analyzer.ts
        │   ├── context/
        │   │   └── docs-parser.ts
        │   └── types/
        │       └── index.ts
        ├── flows/
        │   ├── _template.yaml
        │   ├── login.yaml
        │   └── criar-licitacao.yaml
        ├── screenshots/
        ├── package.json
        ├── tsconfig.json
        └── README.md

Passo 1: Inicialização do Projeto

1.1 Criar a estrutura de diretórios

# Na raiz do seu monorepo
mkdir -p tools/mcp-qa-agent/{src/{tools,playwright,context,types},flows,screenshots}
cd tools/mcp-qa-agent

1.2 Inicializar o projeto Node.js

npm init -y

1.3 Instalar dependências

npm install @modelcontextprotocol/sdk playwright yaml glob gray-matter zod
npm install -D typescript @types/node tsx

1.4 Criar package.json

Substituir o conteúdo do package.json:

{
  "name": "mcp-qa-agent",
  "version": "1.0.0",
  "description": "MCP Server para QA automatizado com Playwright",
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/index.ts",
    "start": "node dist/index.js",
    "postinstall": "npx playwright install chromium"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "glob": "^10.3.10",
    "gray-matter": "^4.0.3",
    "playwright": "^1.40.0",
    "yaml": "^2.3.4",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@types/node": "^20.10.0",
    "tsx": "^4.6.2",
    "typescript": "^5.3.2"
  }
}

1.5 Criar tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "dist",
    "rootDir": "src",
    "declaration": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Passo 2: Tipos e Interfaces

2.1 Criar src/types/index.ts

// src/types/index.ts

/**
 * Configuração do MCP QA Agent
 */
export interface QAAgentConfig {
  /** Caminho raiz do projeto monorepo */
  projectRoot: string;
  /** Caminho relativo para pasta de docs (padrão: "docs") */
  docsPath: string;
  /** URL base do frontend (padrão: "http://localhost:3000") */
  baseUrl: string;
  /** Caminho para salvar screenshots */
  screenshotsPath: string;
  /** Caminho para os fluxos de teste */
  flowsPath: string;
  /** Executar em modo headless (padrão: false para debug) */
  headless: boolean;
  /** Timeout padrão em ms (padrão: 30000) */
  defaultTimeout: number;
}

/**
 * Contexto do projeto carregado dos docs
 */
export interface ProjectContext {
  /** Nome do projeto */
  name: string;
  /** Descrição geral */
  description: string;
  /** Módulos identificados */
  modules: ProjectModule[];
  /** Entidades/Models do sistema */
  entities: string[];
  /** Rotas do frontend identificadas */
  routes: RouteInfo[];
  /** Arquivos de doc processados */
  docsProcessed: string[];
  /** Timestamp do carregamento */
  loadedAt: Date;
}

export interface ProjectModule {
  name: string;
  description: string;
  features: string[];
  relatedDocs: string[];
}

export interface RouteInfo {
  path: string;
  name: string;
  module?: string;
  requiresAuth: boolean;
}

/**
 * Definição de um fluxo de teste
 */
export interface TestFlow {
  /** Identificador único do fluxo */
  id: string;
  /** Nome legível */
  name: string;
  /** Descrição do que o fluxo testa */
  description: string;
  /** Módulo relacionado */
  module?: string;
  /** Se requer autenticação */
  requiresAuth: boolean;
  /** Role do usuário para o teste */
  userRole?: string;
  /** Credenciais de teste (se requiresAuth) */
  credentials?: {
    email: string;
    password: string;
  };
  /** Pré-condições descritas */
  preconditions: string[];
  /** URL inicial (relativa ao baseUrl) */
  startUrl: string;
  /** Passos do teste */
  steps: TestStep[];
  /** Validações finais */
  validations: string[];
}

export interface TestStep {
  /** Nome descritivo do passo */
  name: string;
  /** Tipo de ação */
  action: TestAction;
  /** Seletor CSS ou data-testid */
  selector?: string;
  /** URL para navegação */
  url?: string;
  /** Valor para input */
  value?: string;
  /** Campos para fill-form */
  fields?: Record<string, string>;
  /** Timeout específico deste step em ms */
  timeout?: number;
  /** Se deve tirar screenshot após este step */
  screenshot?: boolean;
  /** Se o step é opcional (não falha o fluxo se der erro) */
  optional?: boolean;
}

export type TestAction =
  | 'navigate'
  | 'click'
  | 'fill'
  | 'fill-form'
  | 'select'
  | 'check'
  | 'uncheck'
  | 'wait'
  | 'wait-url'
  | 'assert-visible'
  | 'assert-hidden'
  | 'assert-text'
  | 'assert-url'
  | 'screenshot'
  | 'scroll'
  | 'hover'
  | 'press-key';

/**
 * Log capturado do console do navegador
 */
export interface CapturedLog {
  /** Tipo: log, warn, error, info, debug */
  type: 'log' | 'warn' | 'error' | 'info' | 'debug';
  /** Conteúdo da mensagem */
  message: string;
  /** URL onde ocorreu */
  url: string;
  /** Linha e coluna se disponível */
  location?: {
    url: string;
    lineNumber: number;
    columnNumber: number;
  };
  /** Timestamp */
  timestamp: Date;
  /** Stack trace se for erro */
  stack?: string;
}

/**
 * Requisição de rede capturada
 */
export interface CapturedRequest {
  url: string;
  method: string;
  status?: number;
  statusText?: string;
  /** Se a requisição falhou */
  failed: boolean;
  /** Motivo da falha */
  failureText?: string;
  /** Tempo de resposta em ms */
  timing?: number;
  /** Tipo de recurso */
  resourceType: string;
  /** Timestamp */
  timestamp: Date;
}

/**
 * Resultado de um step executado
 */
export interface StepResult {
  /** Nome do step */
  name: string;
  /** Se passou */
  success: boolean;
  /** Tempo de execução em ms */
  duration: number;
  /** Mensagem de erro se falhou */
  error?: string;
  /** Caminho do screenshot se capturado */
  screenshot?: string;
  /** Se foi pulado */
  skipped: boolean;
}

/**
 * Resultado completo de um fluxo de teste
 */
export interface FlowResult {
  /** ID do fluxo */
  flowId: string;
  /** Nome do fluxo */
  flowName: string;
  /** Se passou completamente */
  success: boolean;
  /** Timestamp de início */
  startedAt: Date;
  /** Timestamp de fim */
  finishedAt: Date;
  /** Duração total em ms */
  totalDuration: number;
  /** Resultados de cada step */
  steps: StepResult[];
  /** Total de steps que passaram */
  passedSteps: number;
  /** Total de steps que falharam */
  failedSteps: number;
  /** Total de steps pulados */
  skippedSteps: number;
  /** Logs do console capturados */
  consoleLogs: CapturedLog[];
  /** Erros de console (filtrados) */
  consoleErrors: CapturedLog[];
  /** Warnings de console (filtrados) */
  consoleWarnings: CapturedLog[];
  /** Requisições capturadas */
  networkRequests: CapturedRequest[];
  /** Requisições que falharam */
  failedRequests: CapturedRequest[];
  /** Screenshots capturados */
  screenshots: string[];
  /** URL final */
  finalUrl: string;
  /** Análise automática dos problemas */
  analysis: ProblemAnalysis;
}

/**
 * Análise automática dos problemas encontrados
 */
export interface ProblemAnalysis {
  /** Resumo geral */
  summary: string;
  /** Problemas identificados */
  problems: IdentifiedProblem[];
  /** Sugestões de correção */
  suggestions: string[];
  /** Severidade geral: passed, warning, failed */
  severity: 'passed' | 'warning' | 'failed';
}

export interface IdentifiedProblem {
  /** Tipo do problema */
  type: 'js-error' | 'vue-warning' | 'react-error' | 'network-error' | 'assertion-failed' | 'timeout';
  /** Descrição */
  description: string;
  /** Onde ocorreu */
  location?: string;
  /** Severidade */
  severity: 'low' | 'medium' | 'high' | 'critical';
  /** Possível causa */
  possibleCause?: string;
  /** Sugestão de fix */
  suggestedFix?: string;
}

/**
 * Resultado de validação rápida de página
 */
export interface PageValidationResult {
  url: string;
  /** Se a página carregou sem erros críticos */
  success: boolean;
  /** Título da página */
  pageTitle: string;
  /** Tempo de carregamento em ms */
  loadTime: number;
  /** Erros de console */
  consoleErrors: CapturedLog[];
  /** Warnings de console */
  consoleWarnings: CapturedLog[];
  /** Requisições com falha */
  failedRequests: CapturedRequest[];
  /** Screenshot da página */
  screenshot: string;
  /** Análise */
  analysis: ProblemAnalysis;
}

Passo 3: Parser de Documentação

3.1 Criar src/context/docs-parser.ts

// src/context/docs-parser.ts

import { glob } from 'glob';
import { readFileSync } from 'fs';
import { join, basename } from 'path';
import matter from 'gray-matter';
import type { ProjectContext, ProjectModule, RouteInfo } from '../types/index.js';

/**
 * Parser de documentação do projeto
 * Lê arquivos .md da pasta docs e extrai informações estruturadas
 */
export class DocsParser {
  private projectRoot: string;
  private docsPath: string;

  constructor(projectRoot: string, docsPath: string = 'docs') {
    this.projectRoot = projectRoot;
    this.docsPath = docsPath;
  }

  /**
   * Carrega e processa toda a documentação do projeto
   */
  async loadProjectContext(): Promise<ProjectContext> {
    const docsFullPath = join(this.projectRoot, this.docsPath);
    const mdFiles = await glob('**/*.md', { cwd: docsFullPath });
    
    const modules: ProjectModule[] = [];
    const entities: Set<string> = new Set();
    const routes: RouteInfo[] = [];
    let projectName = '';
    let projectDescription = '';

    for (const file of mdFiles) {
      const fullPath = join(docsFullPath, file);
      const content = readFileSync(fullPath, 'utf-8');
      const { data: frontmatter, content: markdown } = matter(content);

      // Extrair nome do projeto do README ou doc principal
      if (file.toLowerCase() === 'readme.md' || file.toLowerCase() === 'index.md') {
        projectName = this.extractProjectName(markdown) || frontmatter.title || 'Projeto';
        projectDescription = this.extractDescription(markdown) || frontmatter.description || '';
      }

      // Identificar módulos
      const moduleInfo = this.extractModuleInfo(file, markdown, frontmatter);
      if (moduleInfo) {
        modules.push(moduleInfo);
      }

      // Extrair entidades mencionadas
      const foundEntities = this.extractEntities(markdown);
      foundEntities.forEach(e => entities.add(e));

      // Extrair rotas
      const foundRoutes = this.extractRoutes(markdown);
      routes.push(...foundRoutes);
    }

    return {
      name: projectName,
      description: projectDescription,
      modules: this.deduplicateModules(modules),
      entities: Array.from(entities),
      routes: this.deduplicateRoutes(routes),
      docsProcessed: mdFiles,
      loadedAt: new Date()
    };
  }

  /**
   * Gera um resumo formatado do contexto para o Claude
   */
  formatContextSummary(context: ProjectContext): string {
    let summary = `# Contexto do Projeto: ${context.name}\n\n`;
    
    if (context.description) {
      summary += `## Descrição\n${context.description}\n\n`;
    }

    if (context.modules.length > 0) {
      summary += `## Módulos Identificados (${context.modules.length})\n\n`;
      for (const mod of context.modules) {
        summary += `### ${mod.name}\n`;
        if (mod.description) {
          summary += `${mod.description}\n`;
        }
        if (mod.features.length > 0) {
          summary += `**Features:** ${mod.features.join(', ')}\n`;
        }
        summary += '\n';
      }
    }

    if (context.entities.length > 0) {
      summary += `## Entidades do Sistema\n`;
      summary += context.entities.join(', ') + '\n\n';
    }

    if (context.routes.length > 0) {
      summary += `## Rotas do Frontend (${context.routes.length})\n\n`;
      const authRoutes = context.routes.filter(r => r.requiresAuth);
      const publicRoutes = context.routes.filter(r => !r.requiresAuth);
      
      if (publicRoutes.length > 0) {
        summary += `**Rotas Públicas:**\n`;
        for (const route of publicRoutes) {
          summary += `- \`${route.path}\` - ${route.name}\n`;
        }
        summary += '\n';
      }
      
      if (authRoutes.length > 0) {
        summary += `**Rotas Autenticadas:**\n`;
        for (const route of authRoutes) {
          summary += `- \`${route.path}\` - ${route.name}${route.module ? ` (${route.module})` : ''}\n`;
        }
        summary += '\n';
      }
    }

    summary += `\n---\n`;
    summary += `*Documentos processados: ${context.docsProcessed.length}*\n`;
    summary += `*Carregado em: ${context.loadedAt.toISOString()}*\n`;

    return summary;
  }

  // Métodos auxiliares de extração

  private extractProjectName(markdown: string): string | null {
    const h1Match = markdown.match(/^#\s+(.+)$/m);
    return h1Match ? h1Match[1].trim() : null;
  }

  private extractDescription(markdown: string): string | null {
    // Pega o primeiro parágrafo após o título
    const lines = markdown.split('\n');
    let foundTitle = false;
    let description = '';
    
    for (const line of lines) {
      if (line.startsWith('# ')) {
        foundTitle = true;
        continue;
      }
      if (foundTitle && line.trim() && !line.startsWith('#')) {
        description = line.trim();
        break;
      }
    }
    
    return description || null;
  }

  private extractModuleInfo(
    filename: string, 
    markdown: string, 
    frontmatter: Record<string, any>
  ): ProjectModule | null {
    // Ignora arquivos genéricos
    const genericFiles = ['readme.md', 'index.md', 'changelog.md', 'contributing.md'];
    if (genericFiles.includes(filename.toLowerCase())) {
      return null;
    }

    const name = frontmatter.module || 
                 frontmatter.title || 
                 this.extractProjectName(markdown) ||
                 basename(filename, '.md');

    const features: string[] = [];
    
    // Extrai features de listas após "Features" ou "Funcionalidades"
    const featureSection = markdown.match(/##\s*(Features|Funcionalidades|Recursos)\s*\n([\s\S]*?)(?=\n##|$)/i);
    if (featureSection) {
      const listItems = featureSection[2].match(/^[-*]\s+(.+)$/gm);
      if (listItems) {
        features.push(...listItems.map(item => item.replace(/^[-*]\s+/, '').trim()));
      }
    }

    return {
      name,
      description: frontmatter.description || this.extractDescription(markdown) || '',
      features,
      relatedDocs: [filename]
    };
  }

  private extractEntities(markdown: string): string[] {
    const entities: string[] = [];
    
    // Padrões comuns para identificar entidades
    const patterns = [
      /(?:model|entidade|entity|tabela|table):\s*`?(\w+)`?/gi,
      /##\s*(?:Model|Entidade|Entity):\s*(\w+)/gi,
      /`(\w+)(?:Model|Entity|Table)`/g
    ];

    for (const pattern of patterns) {
      let match;
      while ((match = pattern.exec(markdown)) !== null) {
        entities.push(match[1]);
      }
    }

    // Entidades comuns em sistemas de licitação
    const commonEntities = [
      'Licitacao', 'Licitação', 'Proposta', 'Fornecedor', 
      'Produtor', 'Usuario', 'Usuário', 'Categoria',
      'Item', 'Lance', 'Contrato', 'Documento'
    ];
    
    for (const entity of commonEntities) {
      if (markdown.includes(entity) && !entities.includes(entity)) {
        entities.push(entity);
      }
    }

    return [...new Set(entities)];
  }

  private extractRoutes(markdown: string): RouteInfo[] {
    const routes: RouteInfo[] = [];
    
    // Padrão: /rota/path ou `rota/path`
    const routePatterns = [
      /`(\/[\w\-\/\:\[\]]+)`/g,
      /(?:rota|route|path|url):\s*`?(\/[\w\-\/\:\[\]]+)`?/gi
    ];

    for (const pattern of routePatterns) {
      let match;
      while ((match = pattern.exec(markdown)) !== null) {
        const path = match[1];
        // Ignora rotas de API
        if (path.startsWith('/api/')) continue;
        
        routes.push({
          path,
          name: this.routePathToName(path),
          requiresAuth: this.guessRequiresAuth(path, markdown),
          module: this.guessRouteModule(path)
        });
      }
    }

    return routes;
  }

  private routePathToName(path: string): string {
    return path
      .split('/')
      .filter(Boolean)
      .map(segment => {
        if (segment.startsWith(':') || segment.startsWith('[')) return '';
        return segment.charAt(0).toUpperCase() + segment.slice(1);
      })
      .filter(Boolean)
      .join(' > ') || 'Home';
  }

  private guessRequiresAuth(path: string, markdown: string): boolean {
    const protectedPaths = ['dashboard', 'admin', 'painel', 'minha', 'meu', 'profile', 'configurac'];
    const publicPaths = ['login', 'registro', 'cadastro', 'home', 'sobre', 'contato', 'public'];
    
    const pathLower = path.toLowerCase();
    
    if (publicPaths.some(p => pathLower.includes(p))) return false;
    if (protectedPaths.some(p => pathLower.includes(p))) return true;
    
    // Verifica contexto no markdown
    const pathContext = markdown.substring(
      Math.max(0, markdown.indexOf(path) - 100),
      markdown.indexOf(path) + path.length + 100
    ).toLowerCase();
    
    if (pathContext.includes('autenticad') || pathContext.includes('logado') || pathContext.includes('protected')) {
      return true;
    }
    
    return false;
  }

  private guessRouteModule(path: string): string | undefined {
    const segments = path.split('/').filter(Boolean);
    if (segments.length > 0) {
      const first = segments[0].toLowerCase();
      const moduleMap: Record<string, string> = {
        'licitacoes': 'Licitações',
        'licitacao': 'Licitações',
        'propostas': 'Propostas',
        'proposta': 'Propostas',
        'dashboard': 'Dashboard',
        'admin': 'Administração',
        'config': 'Configurações',
        'relatorios': 'Relatórios',
        'usuarios': 'Usuários'
      };
      return moduleMap[first];
    }
    return undefined;
  }

  private deduplicateModules(modules: ProjectModule[]): ProjectModule[] {
    const map = new Map<string, ProjectModule>();
    for (const mod of modules) {
      const existing = map.get(mod.name);
      if (existing) {
        existing.features = [...new Set([...existing.features, ...mod.features])];
        existing.relatedDocs = [...new Set([...existing.relatedDocs, ...mod.relatedDocs])];
      } else {
        map.set(mod.name, mod);
      }
    }
    return Array.from(map.values());
  }

  private deduplicateRoutes(routes: RouteInfo[]): RouteInfo[] {
    const map = new Map<string, RouteInfo>();
    for (const route of routes) {
      if (!map.has(route.path)) {
        map.set(route.path, route);
      }
    }
    return Array.from(map.values());
  }
}

Passo 4: Gerenciador do Playwright

4.1 Criar src/playwright/browser-manager.ts

// src/playwright/browser-manager.ts

import { chromium, Browser, BrowserContext, Page } from 'playwright';
import type { QAAgentConfig } from '../types/index.js';

/**
 * Gerencia a instância do browser Playwright
 * Singleton para reutilização entre testes
 */
export class BrowserManager {
  private static instance: BrowserManager;
  private browser: Browser | null = null;
  private config: QAAgentConfig;

  private constructor(config: QAAgentConfig) {
    this.config = config;
  }

  static getInstance(config: QAAgentConfig): BrowserManager {
    if (!BrowserManager.instance) {
      BrowserManager.instance = new BrowserManager(config);
    }
    return BrowserManager.instance;
  }

  /**
   * Obtém ou cria a instância do browser
   */
  async getBrowser(): Promise<Browser> {
    if (!this.browser || !this.browser.isConnected()) {
      this.browser = await chromium.launch({
        headless: this.config.headless,
        args: [
          '--disable-web-security',
          '--disable-features=IsolateOrigins,site-per-process'
        ]
      });
    }
    return this.browser;
  }

  /**
   * Cria um novo contexto de navegação
   */
  async createContext(): Promise<BrowserContext> {
    const browser = await this.getBrowser();
    return await browser.newContext({
      viewport: { width: 1920, height: 1080 },
      ignoreHTTPSErrors: true,
      locale: 'pt-BR',
      timezoneId: 'America/Sao_Paulo'
    });
  }

  /**
   * Cria um novo contexto com autenticação
   */
  async createAuthenticatedContext(
    loginUrl: string,
    credentials: { email: string; password: string }
  ): Promise<BrowserContext> {
    const context = await this.createContext();
    const page = await context.newPage();
    
    try {
      await page.goto(`${this.config.baseUrl}${loginUrl}`, {
        waitUntil: 'networkidle',
        timeout: this.config.defaultTimeout
      });

      // Tenta diferentes seletores comuns de login
      const emailSelectors = [
        'input[name="email"]',
        'input[type="email"]',
        '#email',
        '[data-testid="email-input"]'
      ];
      
      const passwordSelectors = [
        'input[name="password"]',
        'input[type="password"]',
        '#password',
        '[data-testid="password-input"]'
      ];

      const submitSelectors = [
        'button[type="submit"]',
        '[data-testid="login-button"]',
        'button:has-text("Entrar")',
        'button:has-text("Login")'
      ];

      // Encontra e preenche email
      for (const selector of emailSelectors) {
        const element = await page.$(selector);
        if (element) {
          await element.fill(credentials.email);
          break;
        }
      }

      // Encontra e preenche senha
      for (const selector of passwordSelectors) {
        const element = await page.$(selector);
        if (element) {
          await element.fill(credentials.password);
          break;
        }
      }

      // Clica em submit
      for (const selector of submitSelectors) {
        const element = await page.$(selector);
        if (element) {
          await element.click();
          break;
        }
      }

      // Aguarda navegação após login
      await page.waitForNavigation({
        waitUntil: 'networkidle',
        timeout: this.config.defaultTimeout
      }).catch(() => {
        // Ignora timeout se já navegou
      });

      await page.close();
      return context;
    } catch (error) {
      await context.close();
      throw new Error(`Falha na autenticação: ${error}`);
    }
  }

  /**
   * Fecha o browser
   */
  async close(): Promise<void> {
    if (this.browser) {
      await this.browser.close();
      this.browser = null;
    }
  }
}

4.2 Criar src/playwright/page-analyzer.ts

// src/playwright/page-analyzer.ts

import { Page, ConsoleMessage, Request, Response } from 'playwright';
import { writeFileSync, mkdirSync, existsSync } from 'fs';
import { join } from 'path';
import type {
  CapturedLog,
  CapturedRequest,
  ProblemAnalysis,
  IdentifiedProblem
} from '../types/index.js';

/**
 * Analisa páginas e captura logs, erros e métricas
 */
export class PageAnalyzer {
  private consoleLogs: CapturedLog[] = [];
  private networkRequests: CapturedRequest[] = [];
  private screenshotsPath: string;
  private screenshotCounter = 0;

  constructor(screenshotsPath: string) {
    this.screenshotsPath = screenshotsPath;
    if (!existsSync(screenshotsPath)) {
      mkdirSync(screenshotsPath, { recursive: true });
    }
  }

  /**
   * Anexa os listeners de captura à página
   */
  attachListeners(page: Page): void {
    // Captura logs do console
    page.on('console', (msg: ConsoleMessage) => {
      this.consoleLogs.push({
        type: this.mapConsoleType(msg.type()),
        message: msg.text(),
        url: page.url(),
        location: msg.location(),
        timestamp: new Date()
      });
    });

    // Captura erros de página (JS errors)
    page.on('pageerror', (error: Error) => {
      this.consoleLogs.push({
        type: 'error',
        message: error.message,
        url: page.url(),
        timestamp: new Date(),
        stack: error.stack
      });
    });

    // Captura requisições
    page.on('requestfinished', async (request: Request) => {
      const response = await request.response();
      const timing = request.timing();
      
      this.networkRequests.push({
        url: request.url(),
        method: request.method(),
        status: response?.status(),
        statusText: response?.statusText(),
        failed: false,
        timing: timing.responseEnd - timing.requestStart,
        resourceType: request.resourceType(),
        timestamp: new Date()
      });
    });

    // Captura requisições que falharam
    page.on('requestfailed', (request: Request) => {
      this.networkRequests.push({
        url: request.url(),
        method: request.method(),
        failed: true,
        failureText: request.failure()?.errorText || 'Unknown error',
        resourceType: request.resourceType(),
        timestamp: new Date()
      });
    });
  }

  /**
   * Tira screenshot e salva
   */
  async takeScreenshot(page: Page, name: string): Promise<string> {
    this.screenshotCounter++;
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const filename = `${this.screenshotCounter.toString().padStart(3, '0')}-${name}-${timestamp}.png`;
    const filepath = join(this.screenshotsPath, filename);
    
    await page.screenshot({
      path: filepath,
      fullPage: false
    });
    
    return filepath;
  }

  /**
   * Retorna logs capturados
   */
  getLogs(): CapturedLog[] {
    return [...this.consoleLogs];
  }

  /**
   * Retorna erros de console
   */
  getErrors(): CapturedLog[] {
    return this.consoleLogs.filter(log => log.type === 'error');
  }

  /**
   * Retorna warnings de console
   */
  getWarnings(): CapturedLog[] {
    return this.consoleLogs.filter(log => log.type === 'warn');
  }

  /**
   * Retorna requisições capturadas
   */
  getRequests(): CapturedRequest[] {
    return [...this.networkRequests];
  }

  /**
   * Retorna requisições que falharam
   */
  getFailedRequests(): CapturedRequest[] {
    return this.networkRequests.filter(req => req.failed || (req.status && req.status >= 400));
  }

  /**
   * Limpa os dados capturados
   */
  clear(): void {
    this.consoleLogs = [];
    this.networkRequests = [];
    this.screenshotCounter = 0;
  }

  /**
   * Analisa os problemas encontrados
   */
  analyzeProblems(): ProblemAnalysis {
    const problems: IdentifiedProblem[] = [];
    const suggestions: string[] = [];
    
    // Analisa erros de console
    for (const error of this.getErrors()) {
      const problem = this.categorizeError(error);
      if (problem) {
        problems.push(problem);
      }
    }

    // Analisa warnings
    for (const warning of this.getWarnings()) {
      const problem = this.categorizeWarning(warning);
      if (problem) {
        problems.push(problem);
      }
    }

    // Analisa requisições com falha
    for (const request of this.getFailedRequests()) {
      problems.push(this.categorizeNetworkError(request));
    }

    // Gera sugestões
    suggestions.push(...this.generateSuggestions(problems));

    // Determina severidade geral
    const hasCritical = problems.some(p => p.severity === 'critical');
    const hasHigh = problems.some(p => p.severity === 'high');
    const hasErrors = this.getErrors().length > 0 || this.getFailedRequests().length > 0;

    let severity: 'passed' | 'warning' | 'failed';
    if (hasCritical || hasHigh || hasErrors) {
      severity = 'failed';
    } else if (problems.length > 0) {
      severity = 'warning';
    } else {
      severity = 'passed';
    }

    return {
      summary: this.generateSummary(problems, severity),
      problems,
      suggestions,
      severity
    };
  }

  // Métodos auxiliares

  private mapConsoleType(type: string): CapturedLog['type'] {
    const map: Record<string, CapturedLog['type']> = {
      'log': 'log',
      'warning': 'warn',
      'error': 'error',
      'info': 'info',
      'debug': 'debug'
    };
    return map[type] || 'log';
  }

  private categorizeError(log: CapturedLog): IdentifiedProblem | null {
    const message = log.message.toLowerCase();

    // Vue warnings/errors
    if (message.includes('[vue warn]') || message.includes('vue')) {
      return {
        type: 'vue-warning',
        description: log.message,
        location: log.location?.url || log.url,
        severity: 'high',
        possibleCause: this.guessVueCause(log.message),
        suggestedFix: this.suggestVueFix(log.message)
      };
    }

    // React errors
    if (message.includes('react') || message.includes('cannot read prop')) {
      return {
        type: 'react-error',
        description: log.message,
        location: log.location?.url || log.url,
        severity: 'high',
        possibleCause: 'Tentativa de acessar propriedade de objeto null/undefined',
        suggestedFix: 'Adicionar verificação de null/undefined ou optional chaining (?.) antes do acesso'
      };
    }

    // Erro JS genérico
    return {
      type: 'js-error',
      description: log.message,
      location: log.location?.url || log.url,
      severity: 'high',
      possibleCause: this.guessJsErrorCause(log.message),
      suggestedFix: this.suggestJsErrorFix(log.message)
    };
  }

  private categorizeWarning(log: CapturedLog): IdentifiedProblem | null {
    const message = log.message.toLowerCase();

    // Vue deprecation ou warning
    if (message.includes('vue') || message.includes('deprecat')) {
      return {
        type: 'vue-warning',
        description: log.message,
        severity: 'medium',
        possibleCause: 'API deprecated ou uso incorreto de feature Vue'
      };
    }

    // Performance warning
    if (message.includes('performance') || message.includes('slow')) {
      return {
        type: 'js-error',
        description: log.message,
        severity: 'low',
        possibleCause: 'Problema de performance detectado pelo browser'
      };
    }

    return null;
  }

  private categorizeNetworkError(request: CapturedRequest): IdentifiedProblem {
    let severity: IdentifiedProblem['severity'] = 'high';
    let possibleCause = '';
    let suggestedFix = '';

    if (request.status === 401) {
      possibleCause = 'Token de autenticação ausente ou expirado';
      suggestedFix = 'Verificar se o token está sendo enviado no header Authorization';
    } else if (request.status === 403) {
      possibleCause = 'Usuário não tem permissão para este recurso';
      suggestedFix = 'Verificar roles/permissions do usuário';
    } else if (request.status === 404) {
      possibleCause = 'Endpoint não existe ou URL incorreta';
      suggestedFix = 'Verificar se a rota está definida no backend e se a URL está correta';
    } else if (request.status === 422) {
      possibleCause = 'Dados enviados inválidos (validação falhou)';
      suggestedFix = 'Verificar o formato dos dados enviados na requisição';
      severity = 'medium';
    } else if (request.status === 500) {
      possibleCause = 'Erro interno no servidor';
      suggestedFix = 'Verificar logs do backend para identificar o erro';
      severity = 'critical';
    } else if (request.failed) {
      if (request.failureText?.includes('CORS')) {
        possibleCause = 'Bloqueio por política CORS';
        suggestedFix = 'Configurar headers CORS no backend para permitir a origem do frontend';
      } else if (request.failureText?.includes('net::ERR')) {
        possibleCause = 'Falha de conexão de rede';
        suggestedFix = 'Verificar se o servidor backend está rodando e acessível';
      }
    }

    return {
      type: 'network-error',
      description: `${request.method} ${request.url} - ${request.status || request.failureText}`,
      severity,
      possibleCause,
      suggestedFix
    };
  }

  private guessVueCause(message: string): string {
    if (message.includes('Invalid prop')) {
      return 'Prop recebendo tipo diferente do esperado';
    }
    if (message.includes('not defined')) {
      return 'Variável ou método não definido no componente';
    }
    if (message.includes('reactive')) {
      return 'Problema com reatividade Vue';
    }
    return 'Erro no componente Vue';
  }

  private suggestVueFix(message: string): string {
    if (message.includes('Invalid prop')) {
      return 'Verificar o tipo da prop no componente pai e adicionar validação ou valor default';
    }
    if (message.includes('not defined')) {
      return 'Declarar a variável/método no setup() ou data()';
    }
    return 'Revisar o código do componente mencionado no erro';
  }

  private guessJsErrorCause(message: string): string {
    if (message.includes('undefined') || message.includes('null')) {
      return 'Tentativa de acessar propriedade de valor null/undefined';
    }
    if (message.includes('not a function')) {
      return 'Tentativa de chamar como função algo que não é função';
    }
    if (message.includes('not defined')) {
      return 'Variável não declarada ou fora do escopo';
    }
    return 'Erro de JavaScript em runtime';
  }

  private suggestJsErrorFix(message: string): string {
    if (message.includes('undefined') || message.includes('null')) {
      return 'Usar optional chaining (?.) ou verificar se o valor existe antes de acessar';
    }
    if (message.includes('not a function')) {
      return 'Verificar se o método existe e está sendo importado corretamente';
    }
    return 'Analisar o stack trace para identificar a linha com problema';
  }

  private generateSuggestions(problems: IdentifiedProblem[]): string[] {
    const suggestions: string[] = [];
    
    const hasNetworkErrors = problems.some(p => p.type === 'network-error');
    const hasJsErrors = problems.some(p => p.type === 'js-error');
    const hasVueWarnings = problems.some(p => p.type === 'vue-warning');

    if (hasNetworkErrors) {
      suggestions.push('Verificar se o backend está rodando e acessível');
      suggestions.push('Conferir se as variáveis de ambiente de API estão configuradas');
    }

    if (hasJsErrors) {
      suggestions.push('Revisar os componentes mencionados nos erros');
      suggestions.push('Adicionar tratamento de erros (try/catch) em operações assíncronas');
    }

    if (hasVueWarnings) {
      suggestions.push('Revisar props e suas tipagens nos componentes afetados');
      suggestions.push('Verificar se todos os dados reativos estão declarados corretamente');
    }

    // Remove sugestões duplicadas dos problemas individuais
    const uniqueSuggestions = [...new Set(suggestions)];
    return uniqueSuggestions;
  }

  private generateSummary(problems: IdentifiedProblem[], severity: 'passed' | 'warning' | 'failed'): string {
    if (severity === 'passed') {
      return 'Nenhum problema detectado. A página está funcionando corretamente.';
    }

    const errorCount = problems.filter(p => p.severity === 'high' || p.severity === 'critical').length;
    const warningCount = problems.filter(p => p.severity === 'medium' || p.severity === 'low').length;

    if (severity === 'failed') {
      return `Encontrados ${errorCount} erro(s) crítico(s) que precisam ser corrigidos${warningCount > 0 ? ` e ${warningCount} warning(s)` : ''}.`;
    }

    return `Encontrados ${warningCount} warning(s) que podem ser melhorados.`;
  }
}

Passo 5: Tools do MCP

5.1 Criar src/tools/load-context.ts

// src/tools/load-context.ts

import { DocsParser } from '../context/docs-parser.js';
import type { QAAgentConfig, ProjectContext } from '../types/index.js';

let cachedContext: ProjectContext | null = null;

/**
 * Carrega o contexto do projeto a partir da documentação
 */
export async function loadProjectContext(config: QAAgentConfig): Promise<{
  success: boolean;
  context?: ProjectContext;
  summary?: string;
  error?: string;
}> {
  try {
    const parser = new DocsParser(config.projectRoot, config.docsPath);
    const context = await parser.loadProjectContext();
    
    // Cache para uso em outras tools
    cachedContext = context;

    const summary = parser.formatContextSummary(context);

    return {
      success: true,
      context,
      summary
    };
  } catch (error) {
    return {
      success: false,
      error: `Erro ao carregar contexto: ${error}`
    };
  }
}

/**
 * Retorna o contexto em cache
 */
export function getCachedContext(): ProjectContext | null {
  return cachedContext;
}

5.2 Criar src/tools/validate-page.ts

// src/tools/validate-page.ts

import { BrowserManager } from '../playwright/browser-manager.js';
import { PageAnalyzer } from '../playwright/page-analyzer.js';
import type { QAAgentConfig, PageValidationResult } from '../types/index.js';

/**
 * Valida uma página específica - teste rápido sem fluxo
 */
export async function validatePage(
  config: QAAgentConfig,
  url: string,
  options: {
    waitForSelector?: string;
    timeout?: number;
    authenticate?: {
      loginUrl: string;
      email: string;
      password: string;
    };
  } = {}
): Promise<PageValidationResult> {
  const browserManager = BrowserManager.getInstance(config);
  const analyzer = new PageAnalyzer(config.screenshotsPath);
  
  const fullUrl = url.startsWith('http') ? url : `${config.baseUrl}${url}`;
  const timeout = options.timeout || config.defaultTimeout;

  let context;
  
  try {
    // Cria contexto (autenticado ou não)
    if (options.authenticate) {
      context = await browserManager.createAuthenticatedContext(
        options.authenticate.loginUrl,
        {
          email: options.authenticate.email,
          password: options.authenticate.password
        }
      );
    } else {
      context = await browserManager.createContext();
    }

    const page = await context.newPage();
    analyzer.attachListeners(page);

    const startTime = Date.now();
    
    // Navega para a página
    await page.goto(fullUrl, {
      waitUntil: 'networkidle',
      timeout
    });

    // Espera seletor específico se fornecido
    if (options.waitForSelector) {
      await page.waitForSelector(options.waitForSelector, { timeout });
    }

    // Pequena pausa para capturar logs atrasados
    await page.waitForTimeout(1000);

    const loadTime = Date.now() - startTime;
    const pageTitle = await page.title();
    
    // Tira screenshot
    const screenshot = await analyzer.takeScreenshot(page, 'validation');

    // Analisa problemas
    const analysis = analyzer.analyzeProblems();

    await context.close();

    return {
      url: fullUrl,
      success: analysis.severity !== 'failed',
      pageTitle,
      loadTime,
      consoleErrors: analyzer.getErrors(),
      consoleWarnings: analyzer.getWarnings(),
      failedRequests: analyzer.getFailedRequests(),
      screenshot,
      analysis
    };
  } catch (error) {
    if (context) {
      await context.close();
    }
    
    return {
      url: fullUrl,
      success: false,
      pageTitle: '',
      loadTime: 0,
      consoleErrors: [{
        type: 'error',
        message: `Erro ao carregar página: ${error}`,
        url: fullUrl,
        timestamp: new Date()
      }],
      consoleWarnings: [],
      failedRequests: [],
      screenshot: '',
      analysis: {
        summary: `Falha crítica ao carregar a página: ${error}`,
        problems: [{
          type: 'js-error',
          description: `${error}`,
          severity: 'critical'
        }],
        suggestions: ['Verificar se a URL está correta', 'Verificar se o servidor está rodando'],
        severity: 'failed'
      }
    };
  }
}

/**
 * Formata o resultado para exibição
 */
export function formatValidationResult(result: PageValidationResult): string {
  const statusIcon = result.success ? '' : '';
  
  let output = `${statusIcon} Validação de Página\n\n`;
  output += `**URL:** ${result.url}\n`;
  output += `**Título:** ${result.pageTitle}\n`;
  output += `**Tempo de carregamento:** ${result.loadTime}ms\n\n`;

  if (result.consoleErrors.length > 0) {
    output += `### 🔴 Erros de Console (${result.consoleErrors.length})\n\n`;
    for (const error of result.consoleErrors) {
      output += `- \`${error.message}\`\n`;
      if (error.location) {
        output += `  📍 ${error.location.url}:${error.location.lineNumber}\n`;
      }
    }
    output += '\n';
  }

  if (result.consoleWarnings.length > 0) {
    output += `### 🟡 Warnings (${result.consoleWarnings.length})\n\n`;
    for (const warning of result.consoleWarnings) {
      output += `- \`${warning.message}\`\n`;
    }
    output += '\n';
  }

  if (result.failedRequests.length > 0) {
    output += `### 🔴 Requisições com Falha (${result.failedRequests.length})\n\n`;
    for (const req of result.failedRequests) {
      output += `- \`${req.method} ${req.url}\`${req.status || req.failureText}\n`;
    }
    output += '\n';
  }

  output += `### 📋 Análise\n\n`;
  output += `**Severidade:** ${result.analysis.severity}\n`;
  output += `**Resumo:** ${result.analysis.summary}\n\n`;

  if (result.analysis.problems.length > 0) {
    output += `**Problemas Identificados:**\n`;
    for (const problem of result.analysis.problems) {
      output += `- [${problem.severity.toUpperCase()}] ${problem.description}\n`;
      if (problem.possibleCause) {
        output += `  💡 Causa: ${problem.possibleCause}\n`;
      }
      if (problem.suggestedFix) {
        output += `  🔧 Fix: ${problem.suggestedFix}\n`;
      }
    }
    output += '\n';
  }

  if (result.analysis.suggestions.length > 0) {
    output += `**Sugestões Gerais:**\n`;
    for (const suggestion of result.analysis.suggestions) {
      output += `- ${suggestion}\n`;
    }
    output += '\n';
  }

  if (result.screenshot) {
    output += `📸 Screenshot: \`${result.screenshot}\`\n`;
  }

  return output;
}

5.3 Criar src/tools/run-flow.ts

// src/tools/run-flow.ts

import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { parse as parseYaml } from 'yaml';
import { BrowserManager } from '../playwright/browser-manager.js';
import { PageAnalyzer } from '../playwright/page-analyzer.js';
import type {
  QAAgentConfig,
  TestFlow,
  TestStep,
  FlowResult,
  StepResult
} from '../types/index.js';

/**
 * Executa um fluxo de teste completo
 */
export async function runTestFlow(
  config: QAAgentConfig,
  flowIdOrPath: string
): Promise<FlowResult> {
  // Carrega o fluxo
  const flow = loadFlow(config, flowIdOrPath);
  if (!flow) {
    throw new Error(`Fluxo não encontrado: ${flowIdOrPath}`);
  }

  const browserManager = BrowserManager.getInstance(config);
  const analyzer = new PageAnalyzer(config.screenshotsPath);
  
  const startedAt = new Date();
  const stepResults: StepResult[] = [];
  const screenshots: string[] = [];
  
  let context;
  let page;
  let success = true;
  let finalUrl = '';

  try {
    // Cria contexto (autenticado ou não)
    if (flow.requiresAuth && flow.credentials) {
      context = await browserManager.createAuthenticatedContext(
        '/login',
        flow.credentials
      );
    } else {
      context = await browserManager.createContext();
    }

    page = await context.newPage();
    analyzer.attachListeners(page);

    // Navega para URL inicial
    const startUrl = flow.startUrl.startsWith('http') 
      ? flow.startUrl 
      : `${config.baseUrl}${flow.startUrl}`;
    
    await page.goto(startUrl, {
      waitUntil: 'networkidle',
      timeout: config.defaultTimeout
    });

    // Executa cada step
    for (const step of flow.steps) {
      const stepStartTime = Date.now();
      let stepSuccess = true;
      let stepError: string | undefined;
      let stepScreenshot: string | undefined;

      try {
        await executeStep(page, step, config);
        
        // Screenshot se configurado
        if (step.screenshot !== false) {
          stepScreenshot = await analyzer.takeScreenshot(page, step.name.replace(/\s+/g, '-'));
          screenshots.push(stepScreenshot);
        }
      } catch (error) {
        stepSuccess = false;
        stepError = `${error}`;
        
        // Screenshot do erro
        stepScreenshot = await analyzer.takeScreenshot(page, `error-${step.name.replace(/\s+/g, '-')}`);
        screenshots.push(stepScreenshot);

        if (!step.optional) {
          success = false;
          // Registra step e interrompe
          stepResults.push({
            name: step.name,
            success: false,
            duration: Date.now() - stepStartTime,
            error: stepError,
            screenshot: stepScreenshot,
            skipped: false
          });
          
          // Marca restantes como pulados
          const currentIndex = flow.steps.indexOf(step);
          for (let i = currentIndex + 1; i < flow.steps.length; i++) {
            stepResults.push({
              name: flow.steps[i].name,
              success: false,
              duration: 0,
              skipped: true
            });
          }
          break;
        }
      }

      stepResults.push({
        name: step.name,
        success: stepSuccess,
        duration: Date.now() - stepStartTime,
        error: stepError,
        screenshot: stepScreenshot,
        skipped: false
      });
    }

    finalUrl = page.url();
    await context.close();

  } catch (error) {
    success = false;
    if (context) {
      await context.close();
    }
  }

  const finishedAt = new Date();
  const analysis = analyzer.analyzeProblems();

  // Atualiza success baseado na análise
  if (analysis.severity === 'failed') {
    success = false;
  }

  return {
    flowId: flow.id,
    flowName: flow.name,
    success,
    startedAt,
    finishedAt,
    totalDuration: finishedAt.getTime() - startedAt.getTime(),
    steps: stepResults,
    passedSteps: stepResults.filter(s => s.success && !s.skipped).length,
    failedSteps: stepResults.filter(s => !s.success && !s.skipped).length,
    skippedSteps: stepResults.filter(s => s.skipped).length,
    consoleLogs: analyzer.getLogs(),
    consoleErrors: analyzer.getErrors(),
    consoleWarnings: analyzer.getWarnings(),
    networkRequests: analyzer.getRequests(),
    failedRequests: analyzer.getFailedRequests(),
    screenshots,
    finalUrl,
    analysis
  };
}

/**
 * Carrega um fluxo do arquivo YAML
 */
function loadFlow(config: QAAgentConfig, flowIdOrPath: string): TestFlow | null {
  // Tenta como caminho direto
  if (existsSync(flowIdOrPath)) {
    const content = readFileSync(flowIdOrPath, 'utf-8');
    return parseYaml(content) as TestFlow;
  }

  // Tenta na pasta de flows
  const flowPath = join(config.flowsPath, `${flowIdOrPath}.yaml`);
  if (existsSync(flowPath)) {
    const content = readFileSync(flowPath, 'utf-8');
    const parsed = parseYaml(content) as TestFlow;
    parsed.id = flowIdOrPath;
    return parsed;
  }

  // Tenta com extensão .yml
  const flowPathYml = join(config.flowsPath, `${flowIdOrPath}.yml`);
  if (existsSync(flowPathYml)) {
    const content = readFileSync(flowPathYml, 'utf-8');
    const parsed = parseYaml(content) as TestFlow;
    parsed.id = flowIdOrPath;
    return parsed;
  }

  return null;
}

/**
 * Executa um step individual
 */
async function executeStep(
  page: any,
  step: TestStep,
  config: QAAgentConfig
): Promise<void> {
  const timeout = step.timeout || config.defaultTimeout;

  switch (step.action) {
    case 'navigate':
      const url = step.url!.startsWith('http') ? step.url! : `${config.baseUrl}${step.url!}`;
      await page.goto(url, { waitUntil: 'networkidle', timeout });
      break;

    case 'click':
      await page.click(step.selector!, { timeout });
      break;

    case 'fill':
      await page.fill(step.selector!, step.value!, { timeout });
      break;

    case 'fill-form':
      if (step.fields) {
        for (const [field, value] of Object.entries(step.fields)) {
          // Tenta diferentes padrões de seletor
          const selectors = [
            `[name="${field}"]`,
            `#${field}`,
            `[data-testid="${field}"]`,
            `[data-testid="${field}-input"]`
          ];
          
          let filled = false;
          for (const selector of selectors) {
            const element = await page.$(selector);
            if (element) {
              await element.fill(processValue(value));
              filled = true;
              break;
            }
          }
          
          if (!filled) {
            throw new Error(`Campo não encontrado: ${field}`);
          }
        }
      }
      break;

    case 'select':
      await page.selectOption(step.selector!, step.value!, { timeout });
      break;

    case 'check':
      await page.check(step.selector!, { timeout });
      break;

    case 'uncheck':
      await page.uncheck(step.selector!, { timeout });
      break;

    case 'wait':
      await page.waitForSelector(step.selector!, { timeout });
      break;

    case 'wait-url':
      await page.waitForURL(step.url!, { timeout });
      break;

    case 'assert-visible':
      await page.waitForSelector(step.selector!, { state: 'visible', timeout });
      break;

    case 'assert-hidden':
      await page.waitForSelector(step.selector!, { state: 'hidden', timeout });
      break;

    case 'assert-text':
      const element = await page.waitForSelector(step.selector!, { timeout });
      const text = await element.textContent();
      if (!text?.includes(step.value!)) {
        throw new Error(`Texto "${step.value}" não encontrado. Texto atual: "${text}"`);
      }
      break;

    case 'assert-url':
      const currentUrl = page.url();
      if (!currentUrl.includes(step.url!)) {
        throw new Error(`URL esperada: "${step.url}", URL atual: "${currentUrl}"`);
      }
      break;

    case 'screenshot':
      // Screenshot é tratado no nível do fluxo
      break;

    case 'scroll':
      if (step.selector) {
        await page.locator(step.selector).scrollIntoViewIfNeeded();
      } else {
        await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
      }
      break;

    case 'hover':
      await page.hover(step.selector!, { timeout });
      break;

    case 'press-key':
      await page.keyboard.press(step.value!);
      break;

    default:
      throw new Error(`Ação não suportada: ${step.action}`);
  }

  // Pausa para estabilização
  await page.waitForTimeout(300);
}

/**
 * Processa valores especiais (datas relativas, etc)
 */
function processValue(value: string): string {
  if (value === '+7days') {
    const date = new Date();
    date.setDate(date.getDate() + 7);
    return date.toISOString().split('T')[0];
  }
  if (value === '+30days') {
    const date = new Date();
    date.setDate(date.getDate() + 30);
    return date.toISOString().split('T')[0];
  }
  if (value === 'today') {
    return new Date().toISOString().split('T')[0];
  }
  return value;
}

/**
 * Formata o resultado do fluxo para exibição
 */
export function formatFlowResult(result: FlowResult): string {
  const statusIcon = result.success ? '' : '';
  
  let output = `${statusIcon} Fluxo "${result.flowName}" ${result.success ? 'PASSOU' : 'FALHOU'}\n\n`;
  
  output += `**Duração total:** ${result.totalDuration}ms\n`;
  output += `**Steps:** ${result.passedSteps} passou | ${result.failedSteps} falhou | ${result.skippedSteps} pulado\n\n`;

  output += `### 📋 Steps Executados\n\n`;
  for (const step of result.steps) {
    const icon = step.skipped ? '⏭️' : (step.success ? '' : '');
    output += `${icon} **${step.name}**`;
    if (!step.skipped) {
      output += ` (${step.duration}ms)`;
    }
    if (step.error) {
      output += `\n   ⚠️ ${step.error}`;
    }
    output += '\n';
  }
  output += '\n';

  if (result.consoleErrors.length > 0) {
    output += `### 🔴 Erros de Console (${result.consoleErrors.length})\n\n`;
    for (const error of result.consoleErrors.slice(0, 10)) {
      output += `- \`${error.message.substring(0, 200)}\`\n`;
      if (error.location) {
        output += `  📍 ${error.location.url}:${error.location.lineNumber}\n`;
      }
    }
    if (result.consoleErrors.length > 10) {
      output += `\n... e mais ${result.consoleErrors.length - 10} erros\n`;
    }
    output += '\n';
  }

  if (result.consoleWarnings.length > 0) {
    output += `### 🟡 Warnings (${result.consoleWarnings.length})\n\n`;
    for (const warning of result.consoleWarnings.slice(0, 5)) {
      output += `- \`${warning.message.substring(0, 200)}\`\n`;
    }
    if (result.consoleWarnings.length > 5) {
      output += `\n... e mais ${result.consoleWarnings.length - 5} warnings\n`;
    }
    output += '\n';
  }

  if (result.failedRequests.length > 0) {
    output += `### 🔴 Requisições com Falha (${result.failedRequests.length})\n\n`;
    for (const req of result.failedRequests) {
      output += `- \`${req.method} ${req.url}\`${req.status || req.failureText}\n`;
    }
    output += '\n';
  }

  output += `### 📋 Análise\n\n`;
  output += `**Severidade:** ${result.analysis.severity}\n`;
  output += `**Resumo:** ${result.analysis.summary}\n\n`;

  if (result.analysis.problems.length > 0) {
    output += `**Problemas Identificados:**\n`;
    for (const problem of result.analysis.problems) {
      output += `- [${problem.severity.toUpperCase()}] ${problem.description}\n`;
      if (problem.possibleCause) {
        output += `  💡 Causa: ${problem.possibleCause}\n`;
      }
      if (problem.suggestedFix) {
        output += `  🔧 Fix: ${problem.suggestedFix}\n`;
      }
    }
    output += '\n';
  }

  if (result.analysis.suggestions.length > 0) {
    output += `**Sugestões Gerais:**\n`;
    for (const suggestion of result.analysis.suggestions) {
      output += `- ${suggestion}\n`;
    }
    output += '\n';
  }

  output += `**URL Final:** ${result.finalUrl}\n\n`;

  if (result.screenshots.length > 0) {
    output += `### 📸 Screenshots\n`;
    for (const ss of result.screenshots) {
      output += `- \`${ss}\`\n`;
    }
  }

  return output;
}

5.4 Criar src/tools/manage-flows.ts

// src/tools/manage-flows.ts

import { readdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
import { join, basename } from 'path';
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
import type { QAAgentConfig, TestFlow } from '../types/index.js';

/**
 * Lista todos os fluxos disponíveis
 */
export function listFlows(config: QAAgentConfig): {
  flows: Array<{
    id: string;
    name: string;
    description: string;
    module?: string;
    requiresAuth: boolean;
    stepsCount: number;
  }>;
} {
  const flowsDir = config.flowsPath;
  
  if (!existsSync(flowsDir)) {
    return { flows: [] };
  }

  const files = readdirSync(flowsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
  
  const flows = files
    .filter(f => !f.startsWith('_')) // Ignora templates
    .map(file => {
      const content = readFileSync(join(flowsDir, file), 'utf-8');
      const flow = parseYaml(content) as TestFlow;
      const id = basename(file, file.endsWith('.yaml') ? '.yaml' : '.yml');
      
      return {
        id,
        name: flow.name || id,
        description: flow.description || '',
        module: flow.module,
        requiresAuth: flow.requiresAuth || false,
        stepsCount: flow.steps?.length || 0
      };
    });

  return { flows };
}

/**
 * Retorna detalhes de um fluxo específico
 */
export function getFlowDetails(config: QAAgentConfig, flowId: string): TestFlow | null {
  const yamlPath = join(config.flowsPath, `${flowId}.yaml`);
  const ymlPath = join(config.flowsPath, `${flowId}.yml`);
  
  const path = existsSync(yamlPath) ? yamlPath : (existsSync(ymlPath) ? ymlPath : null);
  
  if (!path) {
    return null;
  }

  const content = readFileSync(path, 'utf-8');
  const flow = parseYaml(content) as TestFlow;
  flow.id = flowId;
  
  return flow;
}

/**
 * Cria um novo fluxo de teste
 */
export function createFlow(
  config: QAAgentConfig,
  flow: Omit<TestFlow, 'id'>,
  flowId: string
): { success: boolean; path?: string; error?: string } {
  const flowPath = join(config.flowsPath, `${flowId}.yaml`);
  
  if (existsSync(flowPath)) {
    return {
      success: false,
      error: `Fluxo "${flowId}" já existe`
    };
  }

  try {
    const yaml = stringifyYaml(flow, {
      lineWidth: 0,
      defaultKeyType: 'PLAIN',
      defaultStringType: 'QUOTE_DOUBLE'
    });
    
    writeFileSync(flowPath, yaml, 'utf-8');
    
    return {
      success: true,
      path: flowPath
    };
  } catch (error) {
    return {
      success: false,
      error: `Erro ao criar fluxo: ${error}`
    };
  }
}

/**
 * Formata lista de fluxos para exibição
 */
export function formatFlowsList(flows: ReturnType<typeof listFlows>): string {
  if (flows.flows.length === 0) {
    return 'Nenhum fluxo de teste encontrado.\n\nCrie fluxos na pasta `flows/` ou use `qa_create_flow` para criar um novo.';
  }

  let output = `### 📋 Fluxos de Teste Disponíveis (${flows.flows.length})\n\n`;

  // Agrupa por módulo
  const byModule = new Map<string, typeof flows.flows>();
  
  for (const flow of flows.flows) {
    const module = flow.module || 'Geral';
    if (!byModule.has(module)) {
      byModule.set(module, []);
    }
    byModule.get(module)!.push(flow);
  }

  for (const [module, moduleFlows] of byModule) {
    output += `**${module}**\n`;
    for (const flow of moduleFlows) {
      const authIcon = flow.requiresAuth ? '🔐' : '🌐';
      output += `- ${authIcon} \`${flow.id}\` - ${flow.name} (${flow.stepsCount} steps)\n`;
      if (flow.description) {
        output += `  ${flow.description}\n`;
      }
    }
    output += '\n';
  }

  return output;
}

5.5 Criar src/tools/index.ts

// src/tools/index.ts

export { loadProjectContext, getCachedContext } from './load-context.js';
export { validatePage, formatValidationResult } from './validate-page.js';
export { runTestFlow, formatFlowResult } from './run-flow.js';
export { listFlows, getFlowDetails, createFlow, formatFlowsList } from './manage-flows.js';

Passo 6: MCP Server

6.1 Criar src/server.ts

// src/server.ts

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  Tool
} from '@modelcontextprotocol/sdk/types.js';

import type { QAAgentConfig } from './types/index.js';
import {
  loadProjectContext,
  validatePage,
  formatValidationResult,
  runTestFlow,
  formatFlowResult,
  listFlows,
  formatFlowsList,
  getFlowDetails,
  createFlow
} from './tools/index.js';

/**
 * Cria e configura o MCP Server
 */
export function createMCPServer(config: QAAgentConfig): Server {
  const server = new Server(
    {
      name: 'qa-agent',
      version: '1.0.0'
    },
    {
      capabilities: {
        tools: {}
      }
    }
  );

  // Define as tools disponíveis
  const tools: Tool[] = [
    {
      name: 'qa_load_project_context',
      description: `Carrega e analisa a documentação do projeto para entender a estrutura, módulos, entidades e rotas. 
Use esta ferramenta PRIMEIRO antes de executar testes para entender o contexto do projeto.
Lê arquivos .md da pasta docs/ e retorna um resumo estruturado.`,
      inputSchema: {
        type: 'object',
        properties: {},
        required: []
      }
    },
    {
      name: 'qa_validate_page',
      description: `Valida uma página específica do frontend.
Abre a URL no navegador, captura erros de console, falhas de rede e tira screenshot.
Use para validação rápida de uma página sem executar um fluxo completo.`,
      inputSchema: {
        type: 'object',
        properties: {
          url: {
            type: 'string',
            description: 'URL da página (relativa ao baseUrl ou absoluta)'
          },
          waitForSelector: {
            type: 'string',
            description: 'Seletor CSS para aguardar antes de capturar (opcional)'
          },
          authenticate: {
            type: 'object',
            description: 'Credenciais para autenticação (opcional)',
            properties: {
              loginUrl: {
                type: 'string',
                description: 'URL da página de login (ex: /login)'
              },
              email: {
                type: 'string',
                description: 'Email para login'
              },
              password: {
                type: 'string',
                description: 'Senha para login'
              }
            },
            required: ['loginUrl', 'email', 'password']
          }
        },
        required: ['url']
      }
    },
    {
      name: 'qa_run_flow',
      description: `Executa um fluxo de teste completo definido em YAML.
O fluxo contém uma sequência de passos (navegação, cliques, preenchimento de forms, assertions).
Captura todos os erros de console, falhas de rede e screenshots durante a execução.
Retorna análise detalhada dos problemas encontrados com sugestões de correção.`,
      inputSchema: {
        type: 'object',
        properties: {
          flowId: {
            type: 'string',
            description: 'ID do fluxo (nome do arquivo sem extensão) ou caminho completo para o arquivo YAML'
          }
        },
        required: ['flowId']
      }
    },
    {
      name: 'qa_list_flows',
      description: `Lista todos os fluxos de teste disponíveis.
Mostra ID, nome, descrição, módulo e número de steps de cada fluxo.`,
      inputSchema: {
        type: 'object',
        properties: {},
        required: []
      }
    },
    {
      name: 'qa_get_flow_details',
      description: `Retorna os detalhes completos de um fluxo específico.
Inclui todos os steps, precondições e validações.`,
      inputSchema: {
        type: 'object',
        properties: {
          flowId: {
            type: 'string',
            description: 'ID do fluxo'
          }
        },
        required: ['flowId']
      }
    },
    {
      name: 'qa_create_flow',
      description: `Cria um novo fluxo de teste.
Use para criar fluxos para novas features ou páginas.`,
      inputSchema: {
        type: 'object',
        properties: {
          flowId: {
            type: 'string',
            description: 'ID único para o fluxo (será o nome do arquivo)'
          },
          name: {
            type: 'string',
            description: 'Nome legível do fluxo'
          },
          description: {
            type: 'string',
            description: 'Descrição do que o fluxo testa'
          },
          module: {
            type: 'string',
            description: 'Módulo relacionado (ex: Licitações, Propostas)'
          },
          requiresAuth: {
            type: 'boolean',
            description: 'Se o fluxo requer autenticação'
          },
          startUrl: {
            type: 'string',
            description: 'URL inicial do fluxo'
          },
          steps: {
            type: 'array',
            description: 'Passos do teste',
            items: {
              type: 'object',
              properties: {
                name: { type: 'string' },
                action: { 
                  type: 'string',
                  enum: ['navigate', 'click', 'fill', 'fill-form', 'select', 'check', 'uncheck', 
                         'wait', 'wait-url', 'assert-visible', 'assert-hidden', 'assert-text', 
                         'assert-url', 'screenshot', 'scroll', 'hover', 'press-key']
                },
                selector: { type: 'string' },
                url: { type: 'string' },
                value: { type: 'string' },
                fields: { type: 'object' }
              },
              required: ['name', 'action']
            }
          }
        },
        required: ['flowId', 'name', 'startUrl', 'steps']
      }
    }
  ];

  // Handler para listar tools
  server.setRequestHandler(ListToolsRequestSchema, async () => {
    return { tools };
  });

  // Handler para executar tools
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;

    try {
      switch (name) {
        case 'qa_load_project_context': {
          const result = await loadProjectContext(config);
          if (result.success) {
            return {
              content: [{ type: 'text', text: result.summary! }]
            };
          } else {
            return {
              content: [{ type: 'text', text: `Erro: ${result.error}` }],
              isError: true
            };
          }
        }

        case 'qa_validate_page': {
          const { url, waitForSelector, authenticate } = args as any;
          const result = await validatePage(config, url, {
            waitForSelector,
            authenticate
          });
          return {
            content: [{ type: 'text', text: formatValidationResult(result) }]
          };
        }

        case 'qa_run_flow': {
          const { flowId } = args as any;
          const result = await runTestFlow(config, flowId);
          return {
            content: [{ type: 'text', text: formatFlowResult(result) }]
          };
        }

        case 'qa_list_flows': {
          const flows = listFlows(config);
          return {
            content: [{ type: 'text', text: formatFlowsList(flows) }]
          };
        }

        case 'qa_get_flow_details': {
          const { flowId } = args as any;
          const flow = getFlowDetails(config, flowId);
          if (flow) {
            return {
              content: [{ 
                type: 'text', 
                text: `### Fluxo: ${flow.name}\n\n\`\`\`yaml\n${JSON.stringify(flow, null, 2)}\n\`\`\`` 
              }]
            };
          } else {
            return {
              content: [{ type: 'text', text: `Fluxo "${flowId}" não encontrado.` }],
              isError: true
            };
          }
        }

        case 'qa_create_flow': {
          const { flowId, ...flowData } = args as any;
          const result = createFlow(config, flowData, flowId);
          if (result.success) {
            return {
              content: [{ type: 'text', text: `✅ Fluxo "${flowId}" criado em: ${result.path}` }]
            };
          } else {
            return {
              content: [{ type: 'text', text: `❌ Erro: ${result.error}` }],
              isError: true
            };
          }
        }

        default:
          return {
            content: [{ type: 'text', text: `Tool desconhecida: ${name}` }],
            isError: true
          };
      }
    } catch (error) {
      return {
        content: [{ type: 'text', text: `Erro ao executar ${name}: ${error}` }],
        isError: true
      };
    }
  });

  return server;
}

/**
 * Inicia o servidor MCP via stdio
 */
export async function startServer(config: QAAgentConfig): Promise<void> {
  const server = createMCPServer(config);
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('MCP QA Agent iniciado');
}

6.2 Criar src/index.ts

// src/index.ts

import { resolve } from 'path';
import type { QAAgentConfig } from './types/index.js';
import { startServer } from './server.js';

// Carrega configuração do ambiente
const config: QAAgentConfig = {
  projectRoot: process.env.PROJECT_ROOT || process.cwd(),
  docsPath: process.env.DOCS_PATH || 'docs',
  baseUrl: process.env.BASE_URL || 'http://localhost:3000',
  screenshotsPath: resolve(process.env.SCREENSHOTS_PATH || './screenshots'),
  flowsPath: resolve(process.env.FLOWS_PATH || './flows'),
  headless: process.env.HEADLESS === 'true',
  defaultTimeout: parseInt(process.env.DEFAULT_TIMEOUT || '30000', 10)
};

// Inicia o servidor
startServer(config).catch((error) => {
  console.error('Erro ao iniciar MCP QA Agent:', error);
  process.exit(1);
});

Passo 7: Fluxos de Exemplo

7.1 Criar flows/_template.yaml

# Template de Fluxo de Teste
# Copie este arquivo e renomeie para criar novos fluxos

name: "Nome do Fluxo"
description: "Descrição do que este fluxo testa"
module: "NomeDoModulo"  # Ex: Licitações, Propostas, Dashboard

requiresAuth: false  # true se precisa de login
userRole: "produtor"  # Role do usuário para o teste (se requiresAuth: true)

# Credenciais de teste (se requiresAuth: true)
credentials:
  email: "teste@example.com"
  password: "senha123"

# Pré-condições (documentação)
preconditions:
  - "Servidor frontend rodando em localhost:3000"
  - "Banco de dados com dados de teste"

# URL inicial (relativa ao baseUrl)
startUrl: "/"

# Passos do teste
steps:
  # Navegação
  - name: "Acessar página"
    action: "navigate"
    url: "/pagina"

  # Aguardar elemento
  - name: "Aguardar carregamento"
    action: "wait"
    selector: "[data-testid='conteudo']"

  # Clicar
  - name: "Clicar em botão"
    action: "click"
    selector: "button[data-testid='acao']"

  # Preencher campo
  - name: "Preencher input"
    action: "fill"
    selector: "input[name='campo']"
    value: "valor"

  # Preencher formulário completo
  - name: "Preencher formulário"
    action: "fill-form"
    fields:
      titulo: "Título de Teste"
      descricao: "Descrição de teste"
      data: "+7days"  # Valores especiais: +7days, +30days, today

  # Selecionar opção
  - name: "Selecionar categoria"
    action: "select"
    selector: "select[name='categoria']"
    value: "opcao1"

  # Verificar visibilidade
  - name: "Verificar elemento visível"
    action: "assert-visible"
    selector: ".mensagem-sucesso"

  # Verificar texto
  - name: "Verificar texto"
    action: "assert-text"
    selector: ".titulo"
    value: "Texto Esperado"

  # Verificar URL
  - name: "Verificar redirecionamento"
    action: "assert-url"
    url: "/sucesso"

# Validações finais (documentação)
validations:
  - "Nenhum erro no console"
  - "Nenhuma requisição com falha"
  - "Elemento de sucesso visível"

7.2 Criar flows/login.yaml

name: "Login de Usuário"
description: "Testa o fluxo de login com credenciais válidas"
module: "Autenticação"

requiresAuth: false
startUrl: "/login"

preconditions:
  - "Usuário de teste cadastrado no sistema"

steps:
  - name: "Verificar página de login carregada"
    action: "wait"
    selector: "form"
    
  - name: "Verificar campos visíveis"
    action: "assert-visible"
    selector: "input[type='email'], input[name='email']"

  - name: "Preencher email"
    action: "fill"
    selector: "input[type='email'], input[name='email']"
    value: "teste@licitacoes.agro"

  - name: "Preencher senha"
    action: "fill"
    selector: "input[type='password'], input[name='password']"
    value: "teste123"

  - name: "Clicar em Entrar"
    action: "click"
    selector: "button[type='submit']"

  - name: "Aguardar redirecionamento"
    action: "wait"
    selector: "[data-testid='dashboard'], .dashboard"
    timeout: 10000

  - name: "Verificar nome do usuário"
    action: "assert-visible"
    selector: ".user-name, [data-testid='user-name']"
    optional: true

validations:
  - "Sem erros de console"
  - "Redirecionado para dashboard"
  - "Token armazenado (verificar localStorage ou cookie)"

7.3 Criar flows/criar-licitacao.yaml

name: "Criar Nova Licitação"
description: "Testa o fluxo completo de criação de uma licitação"
module: "Licitações"

requiresAuth: true
userRole: "produtor"
credentials:
  email: "produtor@licitacoes.agro"
  password: "teste123"

startUrl: "/dashboard/produtor"

preconditions:
  - "Usuário logado como produtor"
  - "Pelo menos 1 categoria cadastrada no sistema"

steps:
  - name: "Verificar dashboard carregado"
    action: "wait"
    selector: "[data-testid='dashboard-produtor'], .dashboard"

  - name: "Clicar em Nova Licitação"
    action: "click"
    selector: "[data-testid='btn-nova-licitacao'], a[href*='nova-licitacao'], button:has-text('Nova Licitação')"

  - name: "Aguardar formulário"
    action: "wait"
    selector: "form[data-testid='form-licitacao'], form"

  - name: "Preencher dados da licitação"
    action: "fill-form"
    fields:
      titulo: "Compra de Fertilizantes NPK"
      descricao: "Licitação para aquisição de 500kg de fertilizante NPK 20-10-10 para plantio de soja"
      quantidade: "500"
      unidade: "kg"

  - name: "Selecionar categoria"
    action: "click"
    selector: "[data-testid='select-categoria'], select[name='categoria']"
    
  - name: "Escolher Insumos"
    action: "click"
    selector: "[data-testid='categoria-insumos'], option[value='insumos']"
    optional: true

  - name: "Definir data de encerramento"
    action: "fill"
    selector: "input[name='data_encerramento'], input[type='date']"
    value: "+7days"

  - name: "Clicar em Publicar"
    action: "click"
    selector: "[data-testid='btn-publicar'], button[type='submit']:has-text('Publicar')"

  - name: "Aguardar confirmação"
    action: "wait"
    selector: ".toast-success, [data-testid='sucesso'], .alert-success"
    timeout: 10000

  - name: "Verificar redirecionamento para listagem"
    action: "assert-url"
    url: "/licitacoes"
    optional: true

validations:
  - "Licitação criada com sucesso"
  - "Redirecionado para listagem ou detalhe"
  - "Sem erros de validação no console"
  - "Requisição POST /api/licitacoes retornou 201"

Passo 8: Configuração do Claude Code

8.1 Configurar MCP no Claude Desktop/Code

Edite o arquivo de configuração do Claude:

macOS: ~/Library/Application Support/Claude/claude_desktop_config.jsonWindows: %APPDATA%\Claude\claude_desktop_config.jsonLinux: ~/.config/Claude/claude_desktop_config.json

{
  "mcpServers": {
    "qa-agent": {
      "command": "node",
      "args": [
        "/caminho/completo/para/seu-monorepo/tools/mcp-qa-agent/dist/index.js"
      ],
      "env": {
        "PROJECT_ROOT": "/caminho/completo/para/seu-monorepo",
        "DOCS_PATH": "docs",
        "BASE_URL": "http://localhost:3000",
        "SCREENSHOTS_PATH": "/caminho/completo/para/seu-monorepo/tools/mcp-qa-agent/screenshots",
        "FLOWS_PATH": "/caminho/completo/para/seu-monorepo/tools/mcp-qa-agent/flows",
        "HEADLESS": "false",
        "DEFAULT_TIMEOUT": "30000"
      }
    }
  }
}

8.2 Build do projeto

cd tools/mcp-qa-agent
npm install
npm run build

8.3 Testar localmente

# Testar se o MCP inicia corretamente
npm run dev

Passo 9: Uso no Claude Code

9.1 Comandos disponíveis

Após configurar, o Claude Code terá acesso às seguintes ferramentas:

ComandoUso
qa_load_project_context"Carregue o contexto do projeto"
qa_list_flows"Liste os fluxos de teste disponíveis"
qa_validate_page"Valide a página /dashboard"
qa_run_flow"Execute o fluxo de login"
qa_create_flow"Crie um fluxo para testar o cadastro de fornecedor"

9.2 Exemplos de interação

Exemplo 1: Validação rápida após mudança

Você: "Terminei de ajustar o componente de listagem de licitações. 
       Pode validar a página /licitacoes?"

Claude: [Chama qa_validate_page com url="/licitacoes"]
        
        ✅ Validação de Página
        
        URL: http://localhost:3000/licitacoes
        Título: Licitações | Plataforma Agro
        Tempo de carregamento: 1234ms
        
        Nenhum erro de console detectado.
        Nenhuma requisição com falha.
        
        📸 Screenshot: screenshots/001-validation-2024-01-15T10-30-00.png

Exemplo 2: Fluxo completo com erros

Você: "Execute o fluxo de criar licitação"

Claude: [Chama qa_run_flow com flowId="criar-licitacao"]

        ❌ Fluxo "Criar Nova Licitação" FALHOU
        
        Steps: 4 passou | 1 falhou | 2 pulado
        
        📋 Steps Executados
        ✅ Verificar dashboard carregado (1.2s)
        ✅ Clicar em Nova Licitação (0.3s)
        ✅ Aguardar formulário (0.8s)
        ✅ Preencher dados da licitação (1.1s)
        ❌ Selecionar categoria (timeout)
           ⚠️ Elemento não encontrado: [data-testid='select-categoria']
        ⏭️ Definir data de encerramento
        ⏭️ Clicar em Publicar
        
        🔴 Erros de Console (1)
        - `[Vue warn]: Invalid prop "items" - expected Array, got undefined`
          📍 components/CategoriaSelect.vue:42
        
        🔴 Requisições com Falha (1)
        - `GET /api/categorias` → 401 Unauthorized
        
        💡 Análise:
        O select de categorias não carregou porque a requisição 
        GET /api/categorias retornou 401. Provavelmente o token 
        não está sendo enviado no header.
        
        🔧 Sugestão: Verificar se o token está sendo incluído 
        nas requisições autenticadas. Checar o interceptor do axios.

Exemplo 3: Criar fluxo para nova feature

Você: "Criei uma nova página de perfil do fornecedor em /fornecedor/perfil.
       Crie um fluxo de teste para ela."

Claude: Vou criar um fluxo de teste para a página de perfil do fornecedor.
        
        [Chama qa_create_flow com os parâmetros adequados]
        
        ✅ Fluxo "perfil-fornecedor" criado em: flows/perfil-fornecedor.yaml
        
        O fluxo inclui:
        1. Login como fornecedor
        2. Navegação para /fornecedor/perfil
        3. Verificação dos campos do perfil
        4. Edição de dados
        5. Salvamento e confirmação
        
        Quer que eu execute o fluxo agora para validar?

Passo 10: Manutenção e Evolução

10.1 Adicionar novos fluxos

  1. Copie flows/_template.yaml para flows/nome-do-fluxo.yaml
  2. Edite os steps conforme a feature
  3. O fluxo estará disponível automaticamente

10.2 Atualizar seletores

Quando a UI mudar, atualize os seletores nos arquivos YAML. Prefira sempre data-testid para maior estabilidade:

<!-- No componente Vue/React -->
<button data-testid="btn-publicar">Publicar</button>
# No fluxo
- name: "Clicar em Publicar"
  action: "click"
  selector: "[data-testid='btn-publicar']"

10.3 Debugging

Para debug visual, mantenha HEADLESS=false no env. O navegador abrirá e você verá a execução em tempo real.

Para mais logs, adicione DEBUG=true ao env e implemente logging adicional no código.


Checklist de Implementação

  • Criar estrutura de pastas tools/mcp-qa-agent/
  • Inicializar projeto npm
  • Instalar dependências
  • Criar tsconfig.json
  • Implementar tipos (src/types/index.ts)
  • Implementar parser de docs (src/context/docs-parser.ts)
  • Implementar browser manager (src/playwright/browser-manager.ts)
  • Implementar page analyzer (src/playwright/page-analyzer.ts)
  • Implementar tools (src/tools/*.ts)
  • Implementar server MCP (src/server.ts)
  • Implementar entry point (src/index.ts)
  • Criar fluxos de exemplo (flows/*.yaml)
  • Build do projeto (npm run build)
  • Configurar Claude Desktop (claude_desktop_config.json)
  • Testar integração
  • Criar fluxos específicos do projeto

Troubleshooting

Erro: "Playwright browsers not installed"

npx playwright install chromium

Erro: "Cannot find module"

npm run build

MCP não aparece no Claude Code

  1. Verifique se o caminho no claude_desktop_config.json está correto
  2. Reinicie o Claude Code/Desktop
  3. Verifique logs do MCP: npm run dev

Timeout em steps

Aumente o timeout no step específico ou o DEFAULT_TIMEOUT global.

Autenticação falhando

Verifique os seletores do formulário de login no browser-manager.ts e ajuste conforme sua UI.

Copyright © 2026