mcp-lab-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,992 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.js
4
+ import { config } from "dotenv";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { z } from "zod";
8
+ import { spawn } from "child_process";
9
+ import path from "path";
10
+ import fs from "fs";
11
+ var PROJECT_ROOT = process.cwd();
12
+ config({ path: path.join(PROJECT_ROOT, ".env") });
13
+ var server = new McpServer({
14
+ name: "mcp-lab-agent",
15
+ version: "1.0.0"
16
+ });
17
+ function detectProjectStructure() {
18
+ const structure = {
19
+ hasTests: false,
20
+ testFrameworks: [],
21
+ testDirs: [],
22
+ hasBackend: false,
23
+ backendDir: null,
24
+ hasFrontend: false,
25
+ frontendDir: null,
26
+ hasMobile: false,
27
+ packageJson: null,
28
+ pythonRequirements: null
29
+ };
30
+ const pkgPath = path.join(PROJECT_ROOT, "package.json");
31
+ if (fs.existsSync(pkgPath)) {
32
+ structure.packageJson = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
33
+ const deps = {
34
+ ...structure.packageJson.dependencies,
35
+ ...structure.packageJson.devDependencies
36
+ };
37
+ if (deps.cypress) {
38
+ structure.testFrameworks.push("cypress");
39
+ structure.hasTests = true;
40
+ }
41
+ if (deps["@playwright/test"] || deps.playwright) {
42
+ structure.testFrameworks.push("playwright");
43
+ structure.hasTests = true;
44
+ }
45
+ if (deps.webdriverio || deps["@wdio/cli"]) {
46
+ structure.testFrameworks.push("webdriverio");
47
+ structure.hasTests = true;
48
+ }
49
+ if (deps.jest) {
50
+ structure.testFrameworks.push("jest");
51
+ structure.hasTests = true;
52
+ }
53
+ if (deps.vitest) {
54
+ structure.testFrameworks.push("vitest");
55
+ structure.hasTests = true;
56
+ }
57
+ if (deps.mocha) {
58
+ structure.testFrameworks.push("mocha");
59
+ structure.hasTests = true;
60
+ }
61
+ if (deps.jasmine) {
62
+ structure.testFrameworks.push("jasmine");
63
+ structure.hasTests = true;
64
+ }
65
+ if (deps.appium || deps["appium-webdriverio"]) {
66
+ structure.testFrameworks.push("appium");
67
+ structure.hasTests = true;
68
+ structure.hasMobile = true;
69
+ }
70
+ if (deps.detox) {
71
+ structure.testFrameworks.push("detox");
72
+ structure.hasTests = true;
73
+ structure.hasMobile = true;
74
+ }
75
+ if (deps.supertest) {
76
+ structure.testFrameworks.push("supertest");
77
+ structure.hasTests = true;
78
+ }
79
+ if (deps["@pactum/pactum"] || deps.pactum) {
80
+ structure.testFrameworks.push("pactum");
81
+ structure.hasTests = true;
82
+ }
83
+ if (deps.express || deps.fastify || deps["@nestjs/core"] || deps.koa) {
84
+ structure.hasBackend = true;
85
+ }
86
+ if (deps.next || deps.react || deps.vue || deps.svelte || deps.angular) {
87
+ structure.hasFrontend = true;
88
+ }
89
+ }
90
+ const requirementsPath = path.join(PROJECT_ROOT, "requirements.txt");
91
+ if (fs.existsSync(requirementsPath)) {
92
+ const requirements = fs.readFileSync(requirementsPath, "utf8");
93
+ structure.pythonRequirements = requirements;
94
+ if (/robotframework/i.test(requirements)) {
95
+ structure.testFrameworks.push("robot");
96
+ structure.hasTests = true;
97
+ }
98
+ if (/pytest/i.test(requirements)) {
99
+ structure.testFrameworks.push("pytest");
100
+ structure.hasTests = true;
101
+ }
102
+ if (/behave/i.test(requirements)) {
103
+ structure.testFrameworks.push("behave");
104
+ structure.hasTests = true;
105
+ }
106
+ if (/requests/i.test(requirements)) {
107
+ structure.hasBackend = true;
108
+ }
109
+ }
110
+ const commonTestDirs = [
111
+ "tests",
112
+ "test",
113
+ "e2e",
114
+ "cypress",
115
+ "playwright",
116
+ "__tests__",
117
+ "specs",
118
+ "spec",
119
+ "integration",
120
+ "unit",
121
+ "functional",
122
+ "robot",
123
+ "features",
124
+ "scenarios",
125
+ "mobile",
126
+ "api"
127
+ ];
128
+ for (const dir of commonTestDirs) {
129
+ const fullPath = path.join(PROJECT_ROOT, dir);
130
+ if (fs.existsSync(fullPath)) {
131
+ structure.testDirs.push(dir);
132
+ }
133
+ }
134
+ const commonBackendDirs = ["backend", "server", "api", "src"];
135
+ for (const dir of commonBackendDirs) {
136
+ const fullPath = path.join(PROJECT_ROOT, dir);
137
+ if (fs.existsSync(fullPath) && !structure.backendDir) {
138
+ const hasServerFile = fs.existsSync(path.join(fullPath, "server.js")) || fs.existsSync(path.join(fullPath, "index.js")) || fs.existsSync(path.join(fullPath, "app.js"));
139
+ if (hasServerFile) {
140
+ structure.backendDir = dir;
141
+ }
142
+ }
143
+ }
144
+ const commonFrontendDirs = ["frontend", "client", "web", "app", "src"];
145
+ for (const dir of commonFrontendDirs) {
146
+ const fullPath = path.join(PROJECT_ROOT, dir);
147
+ if (fs.existsSync(fullPath) && !structure.frontendDir) {
148
+ const hasAppFile = fs.existsSync(path.join(fullPath, "App.js")) || fs.existsSync(path.join(fullPath, "App.tsx")) || fs.existsSync(path.join(fullPath, "index.html"));
149
+ if (hasAppFile) {
150
+ structure.frontendDir = dir;
151
+ }
152
+ }
153
+ }
154
+ return structure;
155
+ }
156
+ server.registerTool(
157
+ "detect_project",
158
+ {
159
+ title: "Detectar estrutura do projeto",
160
+ description: "Analisa o projeto e identifica frameworks de teste, pastas, backend, frontend.",
161
+ inputSchema: z.object({}),
162
+ outputSchema: z.object({
163
+ ok: z.boolean(),
164
+ structure: z.object({
165
+ hasTests: z.boolean(),
166
+ testFrameworks: z.array(z.string()),
167
+ testDirs: z.array(z.string()),
168
+ hasBackend: z.boolean(),
169
+ backendDir: z.string().nullable(),
170
+ hasFrontend: z.boolean(),
171
+ frontendDir: z.string().nullable()
172
+ })
173
+ })
174
+ },
175
+ async () => {
176
+ const structure = detectProjectStructure();
177
+ const summary = [
178
+ `Frameworks de teste: ${structure.testFrameworks.join(", ") || "nenhum"}`,
179
+ `Pastas de teste: ${structure.testDirs.join(", ") || "nenhuma"}`,
180
+ `Backend: ${structure.backendDir || "n\xE3o detectado"}`,
181
+ `Frontend: ${structure.frontendDir || "n\xE3o detectado"}`
182
+ ].join("\n");
183
+ return {
184
+ content: [{ type: "text", text: summary }],
185
+ structuredContent: { ok: true, structure }
186
+ };
187
+ }
188
+ );
189
+ server.registerTool(
190
+ "run_tests",
191
+ {
192
+ title: "Executar testes",
193
+ description: "Roda testes do projeto. Suporta: Cypress, Playwright, WebdriverIO, Jest, Vitest, Mocha, Appium, Detox, Robot Framework, pytest, e mais. Detecta automaticamente.",
194
+ inputSchema: z.object({
195
+ framework: z.enum([
196
+ "cypress",
197
+ "playwright",
198
+ "webdriverio",
199
+ "jest",
200
+ "vitest",
201
+ "mocha",
202
+ "appium",
203
+ "detox",
204
+ "robot",
205
+ "pytest",
206
+ "supertest",
207
+ "pactum",
208
+ "npm"
209
+ ]).optional().describe("Framework espec\xEDfico ou 'npm' para npm test."),
210
+ spec: z.string().optional().describe("Caminho do spec (ex: cypress/e2e/test.cy.js)."),
211
+ suite: z.string().optional().describe("Suite ou pattern (ex: e2e, api).")
212
+ }),
213
+ outputSchema: z.object({
214
+ status: z.enum(["passed", "failed", "not_found"]),
215
+ message: z.string(),
216
+ exitCode: z.number(),
217
+ runOutput: z.string().optional()
218
+ })
219
+ },
220
+ async ({ framework, spec, suite }) => {
221
+ const structure = detectProjectStructure();
222
+ if (!structure.hasTests) {
223
+ return {
224
+ content: [{ type: "text", text: "Nenhum framework de teste detectado no projeto." }],
225
+ structuredContent: {
226
+ status: "not_found",
227
+ message: "No test framework found",
228
+ exitCode: 1
229
+ }
230
+ };
231
+ }
232
+ let selectedFramework = framework;
233
+ if (!selectedFramework && structure.testFrameworks.length > 0) {
234
+ selectedFramework = structure.testFrameworks[0];
235
+ }
236
+ let cmd, args, cwd;
237
+ if (selectedFramework === "cypress") {
238
+ cmd = "npx";
239
+ args = spec ? ["cypress", "run", "--spec", spec] : ["cypress", "run"];
240
+ cwd = structure.testDirs.includes("cypress") ? path.join(PROJECT_ROOT, "cypress") : structure.testDirs[0] ? path.join(PROJECT_ROOT, structure.testDirs[0]) : PROJECT_ROOT;
241
+ } else if (selectedFramework === "playwright") {
242
+ cmd = "npx";
243
+ args = spec ? ["playwright", "test", spec] : ["playwright", "test"];
244
+ cwd = structure.testDirs.includes("playwright") ? path.join(PROJECT_ROOT, "playwright") : structure.testDirs[0] ? path.join(PROJECT_ROOT, structure.testDirs[0]) : PROJECT_ROOT;
245
+ } else if (selectedFramework === "webdriverio") {
246
+ cmd = "npx";
247
+ args = spec ? ["wdio", "run", spec] : ["wdio", "run"];
248
+ cwd = PROJECT_ROOT;
249
+ } else if (selectedFramework === "jest") {
250
+ cmd = "npx";
251
+ args = ["jest"];
252
+ if (spec) args.push(spec);
253
+ cwd = PROJECT_ROOT;
254
+ } else if (selectedFramework === "vitest") {
255
+ cmd = "npx";
256
+ args = ["vitest", "run"];
257
+ if (spec) args.push(spec);
258
+ cwd = PROJECT_ROOT;
259
+ } else if (selectedFramework === "mocha") {
260
+ cmd = "npx";
261
+ args = spec ? ["mocha", spec] : ["mocha"];
262
+ cwd = PROJECT_ROOT;
263
+ } else if (selectedFramework === "appium") {
264
+ cmd = "npx";
265
+ args = spec ? ["wdio", "run", spec] : ["wdio", "run"];
266
+ cwd = PROJECT_ROOT;
267
+ } else if (selectedFramework === "detox") {
268
+ cmd = "npx";
269
+ args = ["detox", "test"];
270
+ if (spec) args.push(spec);
271
+ cwd = PROJECT_ROOT;
272
+ } else if (selectedFramework === "robot") {
273
+ cmd = "robot";
274
+ args = spec ? [spec] : [structure.testDirs[0] || "tests"];
275
+ cwd = PROJECT_ROOT;
276
+ } else if (selectedFramework === "pytest") {
277
+ cmd = "pytest";
278
+ args = spec ? [spec] : [];
279
+ cwd = PROJECT_ROOT;
280
+ } else if (selectedFramework === "supertest" || selectedFramework === "pactum") {
281
+ cmd = "npm";
282
+ args = ["test"];
283
+ cwd = PROJECT_ROOT;
284
+ } else {
285
+ cmd = "npm";
286
+ args = ["test"];
287
+ cwd = PROJECT_ROOT;
288
+ }
289
+ return new Promise((resolve) => {
290
+ const child = spawn(cmd, args, {
291
+ cwd,
292
+ stdio: ["inherit", "pipe", "pipe"],
293
+ shell: process.platform === "win32",
294
+ env: { ...process.env }
295
+ });
296
+ let stdout = "";
297
+ let stderr = "";
298
+ if (child.stdout) {
299
+ child.stdout.on("data", (d) => {
300
+ const s = d.toString();
301
+ stdout += s;
302
+ process.stdout.write(s);
303
+ });
304
+ }
305
+ if (child.stderr) {
306
+ child.stderr.on("data", (d) => {
307
+ const s = d.toString();
308
+ stderr += s;
309
+ process.stderr.write(s);
310
+ });
311
+ }
312
+ child.on("close", (code) => {
313
+ const runOutput = [stdout, stderr].filter(Boolean).join("\n").trim();
314
+ const passed = code === 0;
315
+ resolve({
316
+ content: [{ type: "text", text: passed ? "Testes executados com sucesso." : "Falha na execu\xE7\xE3o dos testes." }],
317
+ structuredContent: {
318
+ status: passed ? "passed" : "failed",
319
+ message: passed ? "Tests passed" : "Tests failed",
320
+ exitCode: code ?? 1,
321
+ runOutput: !passed ? runOutput : void 0
322
+ }
323
+ });
324
+ });
325
+ });
326
+ }
327
+ );
328
+ server.registerTool(
329
+ "read_project",
330
+ {
331
+ title: "Ler estrutura do projeto",
332
+ description: "L\xEA package.json, detecta rotas (se backend), specs existentes e retorna contexto.",
333
+ inputSchema: z.object({}),
334
+ outputSchema: z.object({
335
+ ok: z.boolean(),
336
+ summary: z.string(),
337
+ packageJson: z.object({}).passthrough().optional(),
338
+ testFiles: z.array(z.string()).optional()
339
+ })
340
+ },
341
+ async () => {
342
+ const structure = detectProjectStructure();
343
+ const testFiles = [];
344
+ for (const dir of structure.testDirs) {
345
+ const fullPath = path.join(PROJECT_ROOT, dir);
346
+ const walk = (p, base = "") => {
347
+ if (!fs.existsSync(p)) return;
348
+ const entries = fs.readdirSync(p, { withFileTypes: true });
349
+ for (const e of entries) {
350
+ const rel = base ? `${base}/${e.name}` : e.name;
351
+ if (e.isDirectory()) {
352
+ walk(path.join(p, e.name), rel);
353
+ } else if (e.isFile() && /\.(cy|spec|test)\.(js|ts)$/.test(e.name)) {
354
+ testFiles.push(`${dir}/${rel}`);
355
+ }
356
+ }
357
+ };
358
+ walk(fullPath);
359
+ }
360
+ const summary = [
361
+ `Frameworks: ${structure.testFrameworks.join(", ") || "nenhum"}`,
362
+ `Arquivos de teste: ${testFiles.length}`,
363
+ `Backend: ${structure.backendDir || "n\xE3o detectado"}`,
364
+ `Frontend: ${structure.frontendDir || "n\xE3o detectado"}`
365
+ ].join("\n");
366
+ return {
367
+ content: [{ type: "text", text: summary }],
368
+ structuredContent: {
369
+ ok: true,
370
+ summary,
371
+ packageJson: structure.packageJson,
372
+ testFiles: testFiles.slice(0, 50),
373
+ structure
374
+ }
375
+ };
376
+ }
377
+ );
378
+ server.registerTool(
379
+ "generate_tests",
380
+ {
381
+ title: "Gerar testes com LLM",
382
+ description: "Gera spec de teste usando LLM (requer GROQ_API_KEY, GEMINI_API_KEY ou OPENAI_API_KEY).",
383
+ inputSchema: z.object({
384
+ context: z.string().describe("Contexto do projeto (resultado de read_project ou descri\xE7\xE3o)."),
385
+ request: z.string().describe("O que testar (ex: 'login flow', 'API healthcheck')."),
386
+ framework: z.enum([
387
+ "cypress",
388
+ "playwright",
389
+ "webdriverio",
390
+ "jest",
391
+ "vitest",
392
+ "mocha",
393
+ "appium",
394
+ "robot",
395
+ "pytest",
396
+ "supertest"
397
+ ]).optional().describe("Framework alvo.")
398
+ }),
399
+ outputSchema: z.object({
400
+ ok: z.boolean(),
401
+ specContent: z.string().optional(),
402
+ suggestedFileName: z.string().optional(),
403
+ error: z.string().optional()
404
+ })
405
+ },
406
+ async ({ context, request, framework }) => {
407
+ const structure = detectProjectStructure();
408
+ const fw = framework || structure.testFrameworks[0] || "cypress";
409
+ const GROQ_KEY = process.env.GROQ_API_KEY;
410
+ const GEMINI_KEY = process.env.GEMINI_API_KEY;
411
+ const OPENAI_KEY = process.env.OPENAI_API_KEY || process.env.QA_LAB_LLM_API_KEY;
412
+ if (!GROQ_KEY && !GEMINI_KEY && !OPENAI_KEY) {
413
+ return {
414
+ content: [{ type: "text", text: "Configure GROQ_API_KEY, GEMINI_API_KEY ou OPENAI_API_KEY no .env" }],
415
+ structuredContent: { ok: false, error: "No API key configured" }
416
+ };
417
+ }
418
+ const provider = GROQ_KEY ? "groq" : GEMINI_KEY ? "gemini" : "openai";
419
+ const apiKey = GROQ_KEY || GEMINI_KEY || OPENAI_KEY;
420
+ const baseUrl = provider === "groq" ? "https://api.groq.com/openai/v1" : provider === "gemini" ? "https://generativelanguage.googleapis.com/v1beta" : "https://api.openai.com/v1";
421
+ const model = provider === "groq" ? "llama-3.3-70b-versatile" : provider === "gemini" ? "gemini-1.5-flash" : "gpt-4o-mini";
422
+ const systemPrompt = `Voc\xEA \xE9 um engenheiro de QA especializado em ${fw}. Gere APENAS o c\xF3digo do spec, sem explica\xE7\xF5es.
423
+ Framework: ${fw}
424
+ Regras:
425
+ - Para Cypress: use cy.request() para API, cy.visit() para UI
426
+ - Para Playwright: use test.describe() e test(), fixture { request } para API
427
+ - Para Jest: use describe() e test(), fetch() ou axios para API
428
+ - C\xF3digo limpo, sem coment\xE1rios excessivos
429
+ - Retorne SOMENTE o c\xF3digo JavaScript, sem markdown`;
430
+ const userPrompt = `Contexto do projeto:
431
+ ${context.slice(0, 5e3)}
432
+
433
+ Gere um teste para: ${request}
434
+ Framework: ${fw}`;
435
+ try {
436
+ let specContent;
437
+ if (provider === "gemini") {
438
+ const url = `${baseUrl}/models/${model}:generateContent?key=${apiKey}`;
439
+ const res = await fetch(url, {
440
+ method: "POST",
441
+ headers: { "Content-Type": "application/json" },
442
+ body: JSON.stringify({
443
+ systemInstruction: { parts: [{ text: systemPrompt }] },
444
+ contents: [{ parts: [{ text: userPrompt }] }],
445
+ generationConfig: { temperature: 0.3, maxOutputTokens: 4096 }
446
+ })
447
+ });
448
+ const data = await res.json();
449
+ specContent = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
450
+ } else {
451
+ const res = await fetch(`${baseUrl}/chat/completions`, {
452
+ method: "POST",
453
+ headers: {
454
+ "Content-Type": "application/json",
455
+ Authorization: `Bearer ${apiKey}`
456
+ },
457
+ body: JSON.stringify({
458
+ model,
459
+ messages: [
460
+ { role: "system", content: systemPrompt },
461
+ { role: "user", content: userPrompt }
462
+ ],
463
+ temperature: 0.3,
464
+ max_tokens: 4096
465
+ })
466
+ });
467
+ const data = await res.json();
468
+ specContent = data.choices?.[0]?.message?.content || "";
469
+ }
470
+ specContent = specContent.replace(/^```(?:js|javascript)?\n?/i, "").replace(/\n?```\s*$/i, "").trim();
471
+ const fileName = request.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 40);
472
+ return {
473
+ content: [{ type: "text", text: `Spec gerado (${specContent.length} chars). Use write_test para gravar.` }],
474
+ structuredContent: {
475
+ ok: true,
476
+ specContent,
477
+ suggestedFileName: fileName
478
+ }
479
+ };
480
+ } catch (err) {
481
+ return {
482
+ content: [{ type: "text", text: `Erro ao gerar: ${err.message}` }],
483
+ structuredContent: { ok: false, error: err.message }
484
+ };
485
+ }
486
+ }
487
+ );
488
+ server.registerTool(
489
+ "write_test",
490
+ {
491
+ title: "Escrever arquivo de teste",
492
+ description: "Grava spec no disco. Detecta automaticamente a pasta correta.",
493
+ inputSchema: z.object({
494
+ name: z.string().describe("Nome do arquivo (ex: login-test)."),
495
+ content: z.string().describe("Conte\xFAdo do spec."),
496
+ framework: z.enum(["cypress", "playwright", "jest"]).optional().describe("Framework (detectado automaticamente se omitido)."),
497
+ subdir: z.string().optional().describe("Subpasta (ex: e2e, api). Default: raiz da pasta de testes.")
498
+ }),
499
+ outputSchema: z.object({
500
+ ok: z.boolean(),
501
+ path: z.string().optional(),
502
+ error: z.string().optional()
503
+ })
504
+ },
505
+ async ({ name, content, framework, subdir }) => {
506
+ const structure = detectProjectStructure();
507
+ const fw = framework || structure.testFrameworks[0];
508
+ if (!fw) {
509
+ return {
510
+ content: [{ type: "text", text: "Nenhum framework de teste detectado." }],
511
+ structuredContent: { ok: false, error: "No test framework" }
512
+ };
513
+ }
514
+ const ext = fw === "cypress" ? ".cy.js" : fw === "playwright" ? ".spec.js" : ".test.js";
515
+ const safeName = name.replace(/[^a-z0-9-]/gi, "-").replace(/-+/g, "-").replace(/\.(cy|spec|test)\.js$/i, "");
516
+ const fileName = `${safeName}${ext}`;
517
+ let baseDir;
518
+ if (fw === "cypress") {
519
+ baseDir = structure.testDirs.includes("cypress") ? path.join(PROJECT_ROOT, "cypress") : structure.testDirs.includes("tests") ? path.join(PROJECT_ROOT, "tests", "cypress") : path.join(PROJECT_ROOT, structure.testDirs[0] || "tests");
520
+ } else if (fw === "playwright") {
521
+ baseDir = structure.testDirs.includes("playwright") ? path.join(PROJECT_ROOT, "playwright") : structure.testDirs.includes("tests") ? path.join(PROJECT_ROOT, "tests", "playwright") : path.join(PROJECT_ROOT, structure.testDirs[0] || "tests");
522
+ } else {
523
+ baseDir = path.join(PROJECT_ROOT, structure.testDirs[0] || "tests");
524
+ }
525
+ const targetDir = subdir ? path.join(baseDir, subdir) : baseDir;
526
+ const filePath = path.join(targetDir, fileName);
527
+ try {
528
+ if (!fs.existsSync(targetDir)) {
529
+ fs.mkdirSync(targetDir, { recursive: true });
530
+ }
531
+ fs.writeFileSync(filePath, content, "utf8");
532
+ return {
533
+ content: [{ type: "text", text: `Arquivo gravado: ${filePath}` }],
534
+ structuredContent: { ok: true, path: filePath }
535
+ };
536
+ } catch (err) {
537
+ return {
538
+ content: [{ type: "text", text: `Erro ao gravar: ${err.message}` }],
539
+ structuredContent: { ok: false, error: err.message }
540
+ };
541
+ }
542
+ }
543
+ );
544
+ server.registerTool(
545
+ "analyze_failures",
546
+ {
547
+ title: "Analisar falhas de testes",
548
+ description: "Recebe output de testes e extrai falhas estruturadas.",
549
+ inputSchema: z.object({
550
+ runOutput: z.string().describe("Output do teste (stdout/stderr).")
551
+ }),
552
+ outputSchema: z.object({
553
+ ok: z.boolean(),
554
+ summary: z.string(),
555
+ failures: z.array(z.object({
556
+ test: z.string().optional(),
557
+ message: z.string().optional(),
558
+ stack: z.string().optional()
559
+ })).optional()
560
+ })
561
+ },
562
+ async ({ runOutput }) => {
563
+ const failures = [];
564
+ const lines = runOutput.split("\n");
565
+ for (let i = 0; i < lines.length; i++) {
566
+ const line = lines[i];
567
+ if (/fail|error|assertion/i.test(line)) {
568
+ failures.push({
569
+ test: lines[i - 1] || "unknown",
570
+ message: line.trim(),
571
+ stack: lines.slice(i, i + 5).join("\n")
572
+ });
573
+ }
574
+ }
575
+ const summary = failures.length ? `${failures.length} falha(s) detectada(s).` : "Nenhuma falha detectada.";
576
+ return {
577
+ content: [{ type: "text", text: summary }],
578
+ structuredContent: { ok: true, summary, failures: failures.length ? failures : void 0 }
579
+ };
580
+ }
581
+ );
582
+ server.registerTool(
583
+ "suggest_fix",
584
+ {
585
+ title: "Sugerir corre\xE7\xE3o para falhas",
586
+ description: "Recebe an\xE1lise de falhas e sugere corre\xE7\xF5es (patch, refactor, etc.).",
587
+ inputSchema: z.object({
588
+ failures: z.array(z.object({
589
+ test: z.string().optional(),
590
+ message: z.string().optional(),
591
+ stack: z.string().optional()
592
+ })).describe("Resultado de analyze_failures.")
593
+ }),
594
+ outputSchema: z.object({
595
+ ok: z.boolean(),
596
+ suggestions: z.array(z.object({
597
+ test: z.string().optional(),
598
+ description: z.string(),
599
+ fix: z.string().optional()
600
+ }))
601
+ })
602
+ },
603
+ async ({ failures }) => {
604
+ const suggestions = [];
605
+ for (const f of failures) {
606
+ const msg = f.message || "";
607
+ if (/element not found|selector|timeout/i.test(msg)) {
608
+ suggestions.push({
609
+ test: f.test,
610
+ description: "Elemento n\xE3o encontrado ou timeout",
611
+ fix: "Verifique seletores, adicione waits ou aumente timeout. Use data-testid para seletores mais est\xE1veis."
612
+ });
613
+ } else if (/expected.*to.*but/i.test(msg)) {
614
+ suggestions.push({
615
+ test: f.test,
616
+ description: "Asser\xE7\xE3o falhou",
617
+ fix: "Revise o valor esperado. Verifique se o estado da aplica\xE7\xE3o est\xE1 correto antes da asser\xE7\xE3o."
618
+ });
619
+ } else if (/network|fetch|ECONNREFUSED/i.test(msg)) {
620
+ suggestions.push({
621
+ test: f.test,
622
+ description: "Erro de rede ou API n\xE3o dispon\xEDvel",
623
+ fix: "Verifique se o backend est\xE1 rodando. Confirme a URL e porta da API."
624
+ });
625
+ } else {
626
+ suggestions.push({
627
+ test: f.test,
628
+ description: "Falha detectada",
629
+ fix: "Revise o stack trace e o c\xF3digo do teste."
630
+ });
631
+ }
632
+ }
633
+ return {
634
+ content: [{ type: "text", text: JSON.stringify(suggestions, null, 2) }],
635
+ structuredContent: { ok: true, suggestions }
636
+ };
637
+ }
638
+ );
639
+ server.registerTool(
640
+ "create_bug_report",
641
+ {
642
+ title: "Criar relat\xF3rio de bug",
643
+ description: "Gera um bug report estruturado a partir de falhas de teste.",
644
+ inputSchema: z.object({
645
+ failures: z.array(z.object({
646
+ test: z.string().optional(),
647
+ message: z.string().optional(),
648
+ stack: z.string().optional()
649
+ })).describe("Falhas (de analyze_failures)."),
650
+ title: z.string().optional().describe("T\xEDtulo do bug.")
651
+ }),
652
+ outputSchema: z.object({
653
+ ok: z.boolean(),
654
+ report: z.string(),
655
+ title: z.string()
656
+ })
657
+ },
658
+ async ({ failures, title }) => {
659
+ const bugTitle = title || `Falha em ${failures.length} teste(s)`;
660
+ const lines = [
661
+ `# ${bugTitle}`,
662
+ "",
663
+ "## Resumo",
664
+ "",
665
+ `${failures.length} teste(s) falharam durante a execu\xE7\xE3o.`,
666
+ "",
667
+ "## Falhas detectadas",
668
+ ""
669
+ ];
670
+ failures.forEach((f, i) => {
671
+ lines.push(`### ${i + 1}. ${f.test || "Teste desconhecido"}`);
672
+ lines.push("");
673
+ lines.push(`**Mensagem:** ${f.message || "N/A"}`);
674
+ lines.push("");
675
+ if (f.stack) {
676
+ lines.push("**Stack trace:**");
677
+ lines.push("```");
678
+ lines.push(f.stack);
679
+ lines.push("```");
680
+ lines.push("");
681
+ }
682
+ });
683
+ lines.push("## Pr\xF3ximos passos");
684
+ lines.push("");
685
+ lines.push("- [ ] Reproduzir localmente");
686
+ lines.push("- [ ] Identificar causa raiz");
687
+ lines.push("- [ ] Aplicar corre\xE7\xE3o");
688
+ lines.push("- [ ] Validar com testes");
689
+ const report = lines.join("\n");
690
+ return {
691
+ content: [{ type: "text", text: report }],
692
+ structuredContent: { ok: true, report, title: bugTitle }
693
+ };
694
+ }
695
+ );
696
+ server.registerTool(
697
+ "list_test_files",
698
+ {
699
+ title: "Listar arquivos de teste",
700
+ description: "Lista todos os arquivos de teste do projeto (filtro por framework, suite, etc.).",
701
+ inputSchema: z.object({
702
+ framework: z.enum(["cypress", "playwright", "jest", "all"]).optional().describe("Filtrar por framework."),
703
+ pattern: z.string().optional().describe("Pattern para filtrar (ex: 'login', 'api').")
704
+ }),
705
+ outputSchema: z.object({
706
+ ok: z.boolean(),
707
+ files: z.array(z.string()),
708
+ total: z.number()
709
+ })
710
+ },
711
+ async ({ framework, pattern }) => {
712
+ const structure = detectProjectStructure();
713
+ const allFiles = [];
714
+ for (const dir of structure.testDirs) {
715
+ const fullPath = path.join(PROJECT_ROOT, dir);
716
+ const walk = (p, base = "") => {
717
+ if (!fs.existsSync(p)) return;
718
+ const entries = fs.readdirSync(p, { withFileTypes: true });
719
+ for (const e of entries) {
720
+ const rel = base ? `${base}/${e.name}` : e.name;
721
+ if (e.isDirectory()) {
722
+ walk(path.join(p, e.name), rel);
723
+ } else if (e.isFile()) {
724
+ const isCypress = e.name.endsWith(".cy.js") || e.name.endsWith(".cy.ts");
725
+ const isPlaywright = e.name.endsWith(".spec.js") || e.name.endsWith(".spec.ts");
726
+ const isJest = e.name.endsWith(".test.js") || e.name.endsWith(".test.ts");
727
+ if (isCypress || isPlaywright || isJest) {
728
+ const fw = isCypress ? "cypress" : isPlaywright ? "playwright" : "jest";
729
+ if (!framework || framework === "all" || framework === fw) {
730
+ const filePath = `${dir}/${rel}`;
731
+ if (!pattern || filePath.toLowerCase().includes(pattern.toLowerCase())) {
732
+ allFiles.push(filePath);
733
+ }
734
+ }
735
+ }
736
+ }
737
+ }
738
+ };
739
+ walk(fullPath);
740
+ }
741
+ const summary = `Encontrados ${allFiles.length} arquivo(s) de teste.`;
742
+ return {
743
+ content: [{ type: "text", text: `${summary}
744
+
745
+ ${allFiles.slice(0, 50).join("\n")}` }],
746
+ structuredContent: { ok: true, files: allFiles, total: allFiles.length }
747
+ };
748
+ }
749
+ );
750
+ server.registerTool(
751
+ "run_linter",
752
+ {
753
+ title: "Executar linter",
754
+ description: "Roda ESLint, Prettier ou linter configurado no projeto.",
755
+ inputSchema: z.object({
756
+ fix: z.boolean().optional().describe("Auto-fix (--fix). Default: false."),
757
+ path: z.string().optional().describe("Caminho espec\xEDfico (ex: src/). Default: todo o projeto.")
758
+ }),
759
+ outputSchema: z.object({
760
+ status: z.enum(["passed", "failed", "not_found"]),
761
+ message: z.string(),
762
+ exitCode: z.number(),
763
+ output: z.string().optional()
764
+ })
765
+ },
766
+ async ({ fix, path: targetPath }) => {
767
+ const structure = detectProjectStructure();
768
+ const scripts = structure.packageJson?.scripts || {};
769
+ let cmd, args;
770
+ if (scripts.lint) {
771
+ cmd = "npm";
772
+ args = ["run", "lint"];
773
+ } else if (structure.packageJson?.devDependencies?.eslint || structure.packageJson?.dependencies?.eslint) {
774
+ cmd = "npx";
775
+ args = ["eslint", targetPath || "."];
776
+ if (fix) args.push("--fix");
777
+ } else {
778
+ return {
779
+ content: [{ type: "text", text: "Linter n\xE3o detectado no projeto." }],
780
+ structuredContent: { status: "not_found", message: "No linter found", exitCode: 1 }
781
+ };
782
+ }
783
+ return new Promise((resolve) => {
784
+ const child = spawn(cmd, args, {
785
+ cwd: PROJECT_ROOT,
786
+ stdio: ["inherit", "pipe", "pipe"],
787
+ shell: process.platform === "win32",
788
+ env: { ...process.env }
789
+ });
790
+ let stdout = "";
791
+ let stderr = "";
792
+ if (child.stdout) child.stdout.on("data", (d) => {
793
+ stdout += d.toString();
794
+ });
795
+ if (child.stderr) child.stderr.on("data", (d) => {
796
+ stderr += d.toString();
797
+ });
798
+ child.on("close", (code) => {
799
+ const output = [stdout, stderr].filter(Boolean).join("\n").trim();
800
+ const passed = code === 0;
801
+ resolve({
802
+ content: [{ type: "text", text: passed ? "Linter passou." : "Linter encontrou problemas." }],
803
+ structuredContent: {
804
+ status: passed ? "passed" : "failed",
805
+ message: passed ? "Lint passed" : "Lint failed",
806
+ exitCode: code ?? 1,
807
+ output: !passed ? output : void 0
808
+ }
809
+ });
810
+ });
811
+ });
812
+ }
813
+ );
814
+ server.registerTool(
815
+ "install_dependencies",
816
+ {
817
+ title: "Instalar depend\xEAncias",
818
+ description: "Roda npm install, yarn install ou pnpm install (detecta automaticamente).",
819
+ inputSchema: z.object({
820
+ packageManager: z.enum(["npm", "yarn", "pnpm", "auto"]).optional().describe("Package manager. Default: auto.")
821
+ }),
822
+ outputSchema: z.object({
823
+ status: z.enum(["success", "failed"]),
824
+ message: z.string(),
825
+ exitCode: z.number()
826
+ })
827
+ },
828
+ async ({ packageManager = "auto" }) => {
829
+ let pm = packageManager;
830
+ if (pm === "auto") {
831
+ if (fs.existsSync(path.join(PROJECT_ROOT, "yarn.lock"))) pm = "yarn";
832
+ else if (fs.existsSync(path.join(PROJECT_ROOT, "pnpm-lock.yaml"))) pm = "pnpm";
833
+ else pm = "npm";
834
+ }
835
+ return new Promise((resolve) => {
836
+ const child = spawn(pm, ["install"], {
837
+ cwd: PROJECT_ROOT,
838
+ stdio: "inherit",
839
+ shell: process.platform === "win32",
840
+ env: { ...process.env }
841
+ });
842
+ child.on("close", (code) => {
843
+ const passed = code === 0;
844
+ resolve({
845
+ content: [{ type: "text", text: passed ? "Depend\xEAncias instaladas." : "Erro ao instalar depend\xEAncias." }],
846
+ structuredContent: {
847
+ status: passed ? "success" : "failed",
848
+ message: passed ? "Dependencies installed" : "Install failed",
849
+ exitCode: code ?? 1
850
+ }
851
+ });
852
+ });
853
+ });
854
+ }
855
+ );
856
+ server.registerTool(
857
+ "get_test_coverage",
858
+ {
859
+ title: "Obter cobertura de testes",
860
+ description: "Roda testes com coverage (Jest, Playwright, Cypress com plugin).",
861
+ inputSchema: z.object({
862
+ framework: z.enum(["jest", "playwright", "cypress"]).optional().describe("Framework. Default: detectado automaticamente.")
863
+ }),
864
+ outputSchema: z.object({
865
+ status: z.enum(["success", "failed", "not_supported"]),
866
+ message: z.string(),
867
+ coveragePercent: z.number().optional(),
868
+ output: z.string().optional()
869
+ })
870
+ },
871
+ async ({ framework }) => {
872
+ const structure = detectProjectStructure();
873
+ const fw = framework || structure.testFrameworks[0];
874
+ if (fw === "jest") {
875
+ return new Promise((resolve) => {
876
+ const child = spawn("npx", ["jest", "--coverage"], {
877
+ cwd: PROJECT_ROOT,
878
+ stdio: ["inherit", "pipe", "pipe"],
879
+ shell: process.platform === "win32",
880
+ env: { ...process.env }
881
+ });
882
+ let stdout = "";
883
+ if (child.stdout) child.stdout.on("data", (d) => {
884
+ stdout += d.toString();
885
+ });
886
+ child.on("close", (code) => {
887
+ const coverageMatch = stdout.match(/All files.*?(\d+\.?\d*)/);
888
+ const coveragePercent = coverageMatch ? parseFloat(coverageMatch[1]) : void 0;
889
+ resolve({
890
+ content: [{ type: "text", text: `Coverage: ${coveragePercent || "N/A"}%` }],
891
+ structuredContent: {
892
+ status: code === 0 ? "success" : "failed",
893
+ message: code === 0 ? "Coverage generated" : "Coverage failed",
894
+ coveragePercent,
895
+ output: stdout
896
+ }
897
+ });
898
+ });
899
+ });
900
+ }
901
+ return {
902
+ content: [{ type: "text", text: `Coverage n\xE3o suportado para ${fw} ainda.` }],
903
+ structuredContent: { status: "not_supported", message: "Coverage not supported for this framework" }
904
+ };
905
+ }
906
+ );
907
+ server.registerTool(
908
+ "watch_tests",
909
+ {
910
+ title: "Rodar testes em modo watch",
911
+ description: "Inicia testes em watch mode (Jest, Vitest). \xDAtil para desenvolvimento.",
912
+ inputSchema: z.object({
913
+ framework: z.enum(["jest", "vitest"]).optional().describe("Framework. Default: detectado.")
914
+ }),
915
+ outputSchema: z.object({
916
+ status: z.string(),
917
+ message: z.string()
918
+ })
919
+ },
920
+ async ({ framework }) => {
921
+ const structure = detectProjectStructure();
922
+ const fw = framework || (structure.testFrameworks.includes("jest") ? "jest" : "vitest");
923
+ if (!structure.testFrameworks.includes(fw)) {
924
+ return {
925
+ content: [{ type: "text", text: `${fw} n\xE3o detectado no projeto.` }],
926
+ structuredContent: { status: "not_found", message: "Framework not found" }
927
+ };
928
+ }
929
+ return {
930
+ content: [{ type: "text", text: `Para watch mode, rode manualmente: npx ${fw} --watch` }],
931
+ structuredContent: {
932
+ status: "info",
933
+ message: `Watch mode requires interactive terminal. Run: npx ${fw} --watch`
934
+ }
935
+ };
936
+ }
937
+ );
938
+ server.registerTool(
939
+ "create_test_template",
940
+ {
941
+ title: "Criar template de teste",
942
+ description: "Gera template b\xE1sico de teste (boilerplate) para o framework escolhido.",
943
+ inputSchema: z.object({
944
+ framework: z.enum(["cypress", "playwright", "jest"]).describe("Framework."),
945
+ type: z.enum(["api", "ui", "unit"]).optional().describe("Tipo de teste. Default: api.")
946
+ }),
947
+ outputSchema: z.object({
948
+ ok: z.boolean(),
949
+ template: z.string(),
950
+ suggestedFileName: z.string()
951
+ })
952
+ },
953
+ async ({ framework, type = "api" }) => {
954
+ let template = "";
955
+ let fileName = "";
956
+ if (framework === "cypress") {
957
+ fileName = `${type}-test.cy.js`;
958
+ template = `describe('${type.toUpperCase()} Test', () => {
959
+ it('should pass', () => {
960
+ ${type === "api" ? "cy.request('GET', 'http://localhost:3000/api/health').then((res) => {\n expect(res.status).to.eq(200);\n });" : "cy.visit('/');\n cy.get('h1').should('be.visible');"}
961
+ });
962
+ });`;
963
+ } else if (framework === "playwright") {
964
+ fileName = `${type}-test.spec.js`;
965
+ template = `const { test, expect } = require('@playwright/test');
966
+
967
+ test.describe('${type.toUpperCase()} Test', () => {
968
+ test('should pass', async ({ ${type === "api" ? "request" : "page"} }) => {
969
+ ${type === "api" ? "const res = await request.get('http://localhost:3000/api/health');\n expect(res.status()).toBe(200);" : "await page.goto('/');\n await expect(page.locator('h1')).toBeVisible();"}
970
+ });
971
+ });`;
972
+ } else {
973
+ fileName = `${type}-test.test.js`;
974
+ template = `describe('${type.toUpperCase()} Test', () => {
975
+ test('should pass', ${type === "api" ? "async () => {\n const res = await fetch('http://localhost:3000/api/health');\n expect(res.status).toBe(200);\n }" : "() => {\n expect(true).toBe(true);\n }"});
976
+ });`;
977
+ }
978
+ return {
979
+ content: [{ type: "text", text: `Template criado. Use write_test para gravar.` }],
980
+ structuredContent: { ok: true, template, suggestedFileName: fileName }
981
+ };
982
+ }
983
+ );
984
+ async function main() {
985
+ const transport = new StdioServerTransport();
986
+ await server.connect(transport);
987
+ }
988
+ main().catch((err) => {
989
+ console.error("Erro no MCP server:", err);
990
+ process.exit(1);
991
+ });
992
+ //# sourceMappingURL=index.js.map