mcp-lab-agent 2.1.2 → 2.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-lab-agent",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "description": "Autonomous QA agent: executor + intelligent consultant - analyzes, predicts, recommends and learns (modular)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -55,7 +55,8 @@
55
55
  "zod": "^3.25.0"
56
56
  },
57
57
  "optionalDependencies": {
58
- "playwright": "^1.49.0"
58
+ "playwright": "^1.49.0",
59
+ "@slack/bolt": "^3.21.0"
59
60
  },
60
61
  "devDependencies": {
61
62
  "@vitest/coverage-v8": "^2.1.0",
@@ -2,6 +2,14 @@
2
2
 
3
3
  Bot Slack que analisa projetos e gera testes via **mcp-lab-agent**.
4
4
 
5
+ ## Rodar sem clonar o projeto
6
+
7
+ ```bash
8
+ npx mcp-lab-agent slack-bot
9
+ ```
10
+
11
+ Não precisa baixar o repo — o comando instala e executa o bot. Configure `~/.cursor/mcp.json` (ver seção abaixo).
12
+
5
13
  ## Configuração (3 passos)
6
14
 
7
15
  ### 1. Crie o bot no Slack
@@ -12,18 +20,26 @@ Bot Slack que analisa projetos e gera testes via **mcp-lab-agent**.
12
20
  4. **Install to Workspace** → copie o token (`xoxb-...`)
13
21
  5. **Basic Information** → copie o **Signing Secret**
14
22
 
15
- ### 2. Configure o .env
23
+ ### 2. Configure tokens
16
24
 
17
- ```bash
18
- cd slack-bot
19
- cp .env.example .env
25
+ **Opção A** — No `~/.cursor/mcp.json` (recomendado):
26
+
27
+ ```json
28
+ {
29
+ "qa-lab-agent": {
30
+ "slack": {
31
+ "botToken": "xoxb-seu-token",
32
+ "signingSecret": "seu-secret"
33
+ }
34
+ }
35
+ }
20
36
  ```
21
37
 
22
- Edite `.env` e preencha:
38
+ **Opção B** — Via `.env` (se rodar da pasta slack-bot):
23
39
 
24
- ```
25
- SLACK_BOT_TOKEN=xoxb-seu-token
26
- SLACK_SIGNING_SECRET=seu-secret
40
+ ```bash
41
+ cd slack-bot && cp .env.example .env
42
+ # Edite .env com SLACK_BOT_TOKEN e SLACK_SIGNING_SECRET
27
43
  ```
28
44
 
29
45
  ### 3. Configure o repositório
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Verifica se a config do Slack está correta.
4
+ * Rode: node check-config.js
5
+ */
6
+ import { readFileSync, existsSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const home = process.env.HOME || process.env.USERPROFILE;
12
+ const mcpPath = home ? path.join(home, ".cursor", "mcp.json") : null;
13
+
14
+ console.log("\n🔧 QA Lab Slack Bot - Diagnóstico\n");
15
+ console.log("1. mcp.json:");
16
+ if (!mcpPath || !existsSync(mcpPath)) {
17
+ console.log(" ❌ Não encontrado em", mcpPath || "(HOME não definido)");
18
+ process.exit(1);
19
+ }
20
+ console.log(" ✅ Encontrado:", mcpPath);
21
+
22
+ let mcp;
23
+ try {
24
+ mcp = JSON.parse(readFileSync(mcpPath, "utf8"));
25
+ } catch (e) {
26
+ console.log(" ❌ Erro ao ler JSON:", e.message);
27
+ process.exit(1);
28
+ }
29
+
30
+ const slack = mcp?.["qa-lab-agent"]?.slack;
31
+ if (!slack) {
32
+ console.log(" ❌ Seção 'qa-lab-agent.slack' não encontrada");
33
+ console.log(" Estrutura esperada:");
34
+ console.log(' { "qa-lab-agent": { "slack": { "botToken": "xoxb-...", "signingSecret": "..." } } }');
35
+ process.exit(1);
36
+ }
37
+ console.log(" ✅ Config slack encontrada");
38
+
39
+ console.log("\n2. botToken:");
40
+ const token = slack.botToken || slack.SLACK_BOT_TOKEN;
41
+ if (!token) {
42
+ console.log(" ❌ Ausente. Adicione 'botToken' ou 'SLACK_BOT_TOKEN'");
43
+ } else if (!token.startsWith("xoxb-")) {
44
+ console.log(" ⚠️ Deve começar com 'xoxb-'. Você usou o Client Secret?");
45
+ console.log(" Use: OAuth & Permissions → Bot User OAuth Token (depois de Install to Workspace)");
46
+ } else {
47
+ console.log(" ✅ OK (xoxb-...)");
48
+ }
49
+
50
+ console.log("\n3. signingSecret:");
51
+ const secret = slack.signingSecret || slack.SLACK_SIGNING_SECRET;
52
+ if (!secret) {
53
+ console.log(" ❌ Ausente. Adicione 'signingSecret'");
54
+ console.log(" Onde: Basic Information → App Credentials → Signing Secret (Show)");
55
+ } else if (secret === "..." || secret.length < 20) {
56
+ console.log(" ⚠️ Parece placeholder ou inválido. Use o valor real do Signing Secret.");
57
+ } else {
58
+ console.log(" ✅ OK");
59
+ }
60
+
61
+ console.log("\n4. Event Subscriptions (api.slack.com):");
62
+ console.log(" • Request URL deve ser: https://SEU_DOMINIO/slack/events");
63
+ console.log(" • Se local: use ngrok → ngrok http 3000");
64
+ console.log(" • Bot event: app_mention");
65
+
66
+ console.log("\n5. Bot no canal:");
67
+ console.log(" • Mencione o bot no canal ou use /invite @NomeDoBot");
68
+
69
+ if (token && secret && token.startsWith("xoxb-")) {
70
+ console.log("\n✅ Config parece OK. Rode: npm start");
71
+ } else {
72
+ console.log("\n❌ Corrija os itens acima e tente novamente.");
73
+ }
74
+ console.log("");
@@ -7,7 +7,8 @@
7
7
  "scripts": {
8
8
  "start": "node src/index.js",
9
9
  "dev": "node --watch src/index.js",
10
- "setup": "node setup.js"
10
+ "setup": "node setup.js",
11
+ "check": "node check-config.js"
11
12
  },
12
13
  "keywords": ["slack", "qa", "mcp-lab-agent", "testing", "bot"],
13
14
  "author": "",
@@ -9,37 +9,86 @@ const SLACK_BOT_DIR = path.dirname(__dirname);
9
9
 
10
10
  config({ path: path.join(SLACK_BOT_DIR, ".env") });
11
11
 
12
- function loadSlackConfig() {
13
- const configPath = process.env.QA_LAB_CONFIG || path.join(ROOT, "qa-lab-agent.config.json");
14
- if (!existsSync(configPath)) {
15
- return null;
16
- }
12
+ function getMcpJsonPath() {
13
+ const home = process.env.HOME || process.env.USERPROFILE;
14
+ if (!home) return null;
15
+ return path.join(home, ".cursor", "mcp.json");
16
+ }
17
+
18
+ function loadMcpConfig() {
19
+ const mcpPath = process.env.QA_LAB_MCP_CONFIG || getMcpJsonPath();
20
+ if (!mcpPath || !existsSync(mcpPath)) return null;
17
21
  try {
18
- const data = JSON.parse(readFileSync(configPath, "utf8"));
19
- return data.slack || data;
22
+ return JSON.parse(readFileSync(mcpPath, "utf8"));
20
23
  } catch {
21
24
  return null;
22
25
  }
23
26
  }
24
27
 
28
+ function getSlackConfigFromMcp() {
29
+ const mcp = loadMcpConfig();
30
+ const qa = mcp?.["qa-lab-agent"];
31
+ const slack = qa?.slack;
32
+ if (!slack) return null;
33
+ return {
34
+ id: slack.id || slack.channelId,
35
+ botToken: slack.botToken || slack.SLACK_BOT_TOKEN,
36
+ signingSecret: slack.signingSecret || slack.SLACK_SIGNING_SECRET,
37
+ repo: slack.repo || slack.REPO_URL,
38
+ branch: slack.branch || slack.REPO_BRANCH || "main",
39
+ useLocal: !!slack.useLocal,
40
+ workDir: slack.workDir,
41
+ };
42
+ }
43
+
44
+ function loadSlackConfig() {
45
+ const configPath = process.env.QA_LAB_CONFIG || path.join(ROOT, "qa-lab-agent.config.json");
46
+ if (existsSync(configPath)) {
47
+ try {
48
+ const data = JSON.parse(readFileSync(configPath, "utf8"));
49
+ return data.slack || data;
50
+ } catch {}
51
+ }
52
+ return null;
53
+ }
54
+
55
+ function getSlackTokens() {
56
+ const fromMcp = getSlackConfigFromMcp();
57
+ if (fromMcp?.botToken && fromMcp?.signingSecret) {
58
+ return { token: fromMcp.botToken, signingSecret: fromMcp.signingSecret };
59
+ }
60
+ return {
61
+ token: process.env.SLACK_BOT_TOKEN,
62
+ signingSecret: process.env.SLACK_SIGNING_SECRET,
63
+ };
64
+ }
65
+
25
66
  function getRepoForChannel() {
67
+ const fromMcp = getSlackConfigFromMcp();
68
+ if (fromMcp?.useLocal) {
69
+ const workDir = fromMcp.workDir || process.env.WORK_DIR || process.cwd();
70
+ return { useLocal: true, workDir };
71
+ }
72
+ if (fromMcp?.repo) {
73
+ return { url: fromMcp.repo, branch: fromMcp.branch || "main" };
74
+ }
26
75
  const repoUrl = process.env.REPO_URL;
27
- const repoBranch = process.env.REPO_BRANCH || "main";
28
76
  if (repoUrl) {
29
- return { url: repoUrl, branch: repoBranch };
77
+ return { url: repoUrl, branch: process.env.REPO_BRANCH || "main" };
30
78
  }
31
79
  const cfg = loadSlackConfig();
32
80
  const repo = cfg?.repo || cfg?.defaultRepo?.url;
33
81
  const branch = cfg?.branch || cfg?.defaultRepo?.branch || "main";
34
82
  if (!repo) {
35
- throw new Error("Configure REPO_URL no .env ou 'repo' no qa-lab-agent.config.json");
83
+ return { useLocal: true, workDir: process.env.WORK_DIR || process.cwd() };
36
84
  }
37
85
  return { url: repo, branch };
38
86
  }
39
87
 
40
88
  function getMcpLabAgentCmd() {
89
+ const fromMcp = loadMcpConfig()?.["qa-lab-agent"]?.mcpLabAgent;
41
90
  const cfg = loadSlackConfig();
42
- const mcp = cfg?.mcpLabAgent || { command: "npx", args: ["-y", "mcp-lab-agent@latest"] };
91
+ const mcp = fromMcp || cfg?.mcpLabAgent || { command: "npx", args: ["-y", "mcp-lab-agent@latest"] };
43
92
  return { command: mcp.command, args: mcp.args || [] };
44
93
  }
45
94
 
@@ -47,4 +96,11 @@ function getCloneBaseDir() {
47
96
  return process.env.CLONE_BASE_DIR || path.join(process.cwd(), ".qa-lab-clones");
48
97
  }
49
98
 
50
- export { loadSlackConfig, getRepoForChannel, getMcpLabAgentCmd, getCloneBaseDir };
99
+ export {
100
+ loadSlackConfig,
101
+ getRepoForChannel,
102
+ getMcpLabAgentCmd,
103
+ getCloneBaseDir,
104
+ getSlackConfigFromMcp,
105
+ getSlackTokens,
106
+ };
@@ -3,23 +3,16 @@
3
3
  * QA Lab Slack Bot
4
4
  * Recebe @mentions no Slack, executa mcp-lab-agent e posta relatório.
5
5
  *
6
- * Config: qa-lab-agent.config.json (slack section)
7
- * Secrets: .env (SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET)
6
+ * Config: ~/.cursor/mcp.json (qa-lab-agent.slack) ou .env
8
7
  */
9
8
  import { App } from "@slack/bolt";
10
- import { config } from "dotenv";
11
- import path from "node:path";
12
- import { fileURLToPath } from "node:url";
9
+ import { getSlackTokens } from "./config.js";
13
10
  import { registerAppMention } from "./handlers/app-mention.js";
14
11
 
15
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
- config({ path: path.join(__dirname, "..", ".env") });
17
-
18
- const token = process.env.SLACK_BOT_TOKEN;
19
- const signingSecret = process.env.SLACK_SIGNING_SECRET;
12
+ const { token, signingSecret } = getSlackTokens();
20
13
 
21
14
  if (!token || !signingSecret) {
22
- console.error("Configure SLACK_BOT_TOKEN e SLACK_SIGNING_SECRET no .env");
15
+ console.error("Configure no ~/.cursor/mcp.json:\n \"qa-lab-agent\": { \"slack\": { \"botToken\": \"xoxb-...\", \"signingSecret\": \"...\" } }\n\nOu use .env (SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET)");
23
16
  process.exit(1);
24
17
  }
25
18
 
@@ -70,12 +70,17 @@ export async function runQaJob({ channelId, userMessage }) {
70
70
 
71
71
  const outputs = [];
72
72
  let lastError = null;
73
+ let cwd = workDir;
73
74
 
74
- try {
75
- await ensureRepo(repo.url, repo.branch, workDir);
76
- } catch (err) {
77
- lastError = `Erro ao clonar repositório: ${err.message}`;
78
- return { ok: false, output: lastError, error: err.message };
75
+ if (repo.useLocal) {
76
+ cwd = repo.workDir;
77
+ } else {
78
+ try {
79
+ await ensureRepo(repo.url, repo.branch, workDir);
80
+ } catch (err) {
81
+ lastError = `Erro ao clonar repositório: ${err.message}`;
82
+ return { ok: false, output: lastError, error: err.message };
83
+ }
79
84
  }
80
85
 
81
86
  const intent = parseUserIntent(userMessage);
@@ -83,7 +88,7 @@ export async function runQaJob({ channelId, userMessage }) {
83
88
  try {
84
89
  if (intent.runAuto) {
85
90
  const autoArgs = [...args, "auto", intent.autoDescription];
86
- const res = await runCommand(command, autoArgs, workDir);
91
+ const res = await runCommand(command, autoArgs, cwd);
87
92
  outputs.push("=== mcp-lab-agent auto ===\n" + res.stdout);
88
93
  if (res.stderr) outputs.push(res.stderr);
89
94
  if (res.code !== 0) {
@@ -93,7 +98,7 @@ export async function runQaJob({ channelId, userMessage }) {
93
98
 
94
99
  if (intent.runAnalyze) {
95
100
  const analyzeArgs = [...args, "analyze"];
96
- const res = await runCommand(command, analyzeArgs, workDir);
101
+ const res = await runCommand(command, analyzeArgs, cwd);
97
102
  outputs.push("=== mcp-lab-agent analyze ===\n" + res.stdout);
98
103
  if (res.stderr) outputs.push(res.stderr);
99
104
  }
@@ -101,9 +106,11 @@ export async function runQaJob({ channelId, userMessage }) {
101
106
  lastError = err.message;
102
107
  outputs.push(`Erro: ${err.message}`);
103
108
  } finally {
104
- try {
105
- if (fs.existsSync(workDir)) fs.rmSync(workDir, { recursive: true });
106
- } catch {}
109
+ if (!repo.useLocal) {
110
+ try {
111
+ if (fs.existsSync(workDir)) fs.rmSync(workDir, { recursive: true });
112
+ } catch {}
113
+ }
107
114
  }
108
115
 
109
116
  const output = outputs.join("\n\n").trim() || lastError || "Nenhum output.";