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/README.md +8 -8
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/slack-bot/README.md +24 -8
- package/slack-bot/check-config.js +74 -0
- package/slack-bot/package.json +2 -1
- package/slack-bot/src/config.js +68 -12
- package/slack-bot/src/index.js +4 -11
- package/slack-bot/src/workers/qa-job.js +17 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-lab-agent",
|
|
3
|
-
"version": "2.1.
|
|
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",
|
package/slack-bot/README.md
CHANGED
|
@@ -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
|
|
23
|
+
### 2. Configure tokens
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
38
|
+
**Opção B** — Via `.env` (se rodar da pasta slack-bot):
|
|
23
39
|
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
SLACK_SIGNING_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("");
|
package/slack-bot/package.json
CHANGED
|
@@ -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": "",
|
package/slack-bot/src/config.js
CHANGED
|
@@ -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
|
|
13
|
-
const
|
|
14
|
-
if (!
|
|
15
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
99
|
+
export {
|
|
100
|
+
loadSlackConfig,
|
|
101
|
+
getRepoForChannel,
|
|
102
|
+
getMcpLabAgentCmd,
|
|
103
|
+
getCloneBaseDir,
|
|
104
|
+
getSlackConfigFromMcp,
|
|
105
|
+
getSlackTokens,
|
|
106
|
+
};
|
package/slack-bot/src/index.js
CHANGED
|
@@ -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.
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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.";
|