@debugg-ai/debugg-ai-mcp 1.0.1
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 +2 -0
- package/dist/e2e-agents/e2eRunner.js +169 -0
- package/dist/e2e-agents/recordingHandler.js +57 -0
- package/dist/e2e-agents/resultsFormatter.js +102 -0
- package/dist/index.js +177 -0
- package/dist/services/coverage.js +127 -0
- package/dist/services/e2es.js +170 -0
- package/dist/services/index.js +36 -0
- package/dist/services/issues.js +132 -0
- package/dist/services/repos.js +23 -0
- package/dist/services/types.js +1 -0
- package/dist/src/e2e-agents/e2eRunner.js +127 -0
- package/dist/src/e2e-agents/recordingHandler.js +57 -0
- package/dist/src/e2e-agents/resultsFormatter.js +102 -0
- package/dist/src/index.js +107 -0
- package/dist/src/services/coverage.js +127 -0
- package/dist/src/services/e2es.js +170 -0
- package/dist/src/services/index.js +36 -0
- package/dist/src/services/indexes.js +74 -0
- package/dist/src/services/issues.js +132 -0
- package/dist/src/services/repos.js +23 -0
- package/dist/src/services/types.js +1 -0
- package/dist/src/tunnels/ngrok/error.js +3 -0
- package/dist/src/tunnels/ngrok/index.js +154 -0
- package/dist/src/tunnels/ngrok/statusBarItem.js +25 -0
- package/dist/src/tunnels/ngrok/types.js +2 -0
- package/dist/src/utils/axios.js +35 -0
- package/dist/src/utils/axiosNaming.js +31 -0
- package/dist/src/utils/axiosTransport.js +57 -0
- package/dist/src/utils/objectNaming.js +47 -0
- package/dist/src/utils/transportConfig.js +1 -0
- package/dist/tunnels/ngrok/error.js +3 -0
- package/dist/tunnels/ngrok/index.js +159 -0
- package/dist/tunnels/ngrok/types.js +2 -0
- package/dist/utils/axios.js +35 -0
- package/dist/utils/axiosNaming.js +31 -0
- package/dist/utils/axiosTransport.js +57 -0
- package/dist/utils/objectNaming.js +47 -0
- package/dist/utils/transportConfig.js +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { downloadBinary } from '../tunnels/ngrok/index.js';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import ngrok from 'ngrok';
|
|
4
|
+
async function startTunnel(localPort, domain) {
|
|
5
|
+
try {
|
|
6
|
+
// await start({
|
|
7
|
+
// addr: localPort,
|
|
8
|
+
// hostname: domain,
|
|
9
|
+
// });
|
|
10
|
+
const url = await ngrok.connect({ addr: localPort, hostname: domain });
|
|
11
|
+
return url;
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
console.error('Error starting ngrok tunnel:', err);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function stopTunnel(url) {
|
|
18
|
+
if (url) {
|
|
19
|
+
await ngrok.disconnect(url);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
await ngrok.disconnect();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class E2eTestRunner {
|
|
26
|
+
client;
|
|
27
|
+
constructor(client) {
|
|
28
|
+
this.client = client;
|
|
29
|
+
}
|
|
30
|
+
async setup() {
|
|
31
|
+
await this.configureNgrok();
|
|
32
|
+
}
|
|
33
|
+
async configureNgrok() {
|
|
34
|
+
await downloadBinary();
|
|
35
|
+
}
|
|
36
|
+
async startTunnel(port, url) {
|
|
37
|
+
await startTunnel(port, url);
|
|
38
|
+
console.error(`Tunnel started at: ${url}`);
|
|
39
|
+
return url;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Run E2E test generator for a single file *quietly* in the background.
|
|
43
|
+
* @param filePath absolute path of the file to test
|
|
44
|
+
*/
|
|
45
|
+
async runTests(e2eRun) {
|
|
46
|
+
// Start by opening an ngrok tunnel.
|
|
47
|
+
// call the debugg ai endpoint to start running the test
|
|
48
|
+
// retrieve the results when done
|
|
49
|
+
// save files locally somewhere
|
|
50
|
+
const listener = await startTunnel(3011, `${e2eRun.key}.ngrok.debugg.ai`);
|
|
51
|
+
console.error(`Tunnel started at: ${listener}`);
|
|
52
|
+
const interval = setInterval(async () => {
|
|
53
|
+
const newE2eRun = await this.client.e2es?.getE2eRun(e2eRun.uuid);
|
|
54
|
+
console.error(`E2E run - ${newE2eRun}`);
|
|
55
|
+
if (newE2eRun?.status === 'completed') {
|
|
56
|
+
console.error(`E2E run completed - ${newE2eRun}`);
|
|
57
|
+
clearInterval(interval);
|
|
58
|
+
await stopTunnel(listener);
|
|
59
|
+
}
|
|
60
|
+
}, 1000);
|
|
61
|
+
// if the run doesn't complete in time, disconnect the tunnel
|
|
62
|
+
const setTimer = setTimeout(async () => {
|
|
63
|
+
clearInterval(interval);
|
|
64
|
+
clearTimeout(setTimer);
|
|
65
|
+
await stopTunnel(listener);
|
|
66
|
+
}, 300000);
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create a new E2E test and run it.
|
|
71
|
+
* @param testPort - The port to use for the test.
|
|
72
|
+
* @param testDescription - The description of the test.
|
|
73
|
+
* @param filePath - The path to the file to test.
|
|
74
|
+
* @param repoName - The name of the repository.
|
|
75
|
+
* @param branchName - The name of the branch.
|
|
76
|
+
* @param repoPath - The path to the repository.
|
|
77
|
+
*/
|
|
78
|
+
async createNewE2eTest(testPort, testDescription, repoName, branchName, repoPath, filePath) {
|
|
79
|
+
console.error(`Creating new E2E test with description: ${testDescription}`);
|
|
80
|
+
const key = uuidv4();
|
|
81
|
+
await startTunnel(testPort, `${key}.ngrok.debugg.ai`);
|
|
82
|
+
const e2eTest = await this.client.e2es?.createE2eTest(testDescription, filePath ?? "", repoName, branchName, {
|
|
83
|
+
repoPath: repoPath,
|
|
84
|
+
key: key
|
|
85
|
+
});
|
|
86
|
+
console.error(`E2E test created - ${e2eTest}`);
|
|
87
|
+
if (!e2eTest) {
|
|
88
|
+
console.error("Failed to create E2E test.");
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (!e2eTest.curRun) {
|
|
92
|
+
console.error("Failed to create E2E test run.");
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return e2eTest.curRun;
|
|
96
|
+
}
|
|
97
|
+
async handleE2eRun(e2eRun, onUpdate) {
|
|
98
|
+
console.error(`🔧 Handling E2E run - ${e2eRun.uuid}`);
|
|
99
|
+
console.error(`🌐 Tunnel started at: ${e2eRun.key}.ngrok.debugg.ai`);
|
|
100
|
+
let stopped = false;
|
|
101
|
+
let updatedRun = e2eRun;
|
|
102
|
+
const timeout = setTimeout(async () => {
|
|
103
|
+
if (stopped)
|
|
104
|
+
return;
|
|
105
|
+
clearInterval(interval);
|
|
106
|
+
await stopTunnel(`https://${e2eRun.key}.ngrok.debugg.ai`);
|
|
107
|
+
console.error(`⏰ E2E test timed out after 15 minutes`);
|
|
108
|
+
stopped = true;
|
|
109
|
+
}, 900_000);
|
|
110
|
+
const interval = setInterval(async () => {
|
|
111
|
+
const latestRun = await this.client.e2es?.getE2eRun(e2eRun.uuid);
|
|
112
|
+
if (!latestRun)
|
|
113
|
+
return;
|
|
114
|
+
updatedRun = latestRun;
|
|
115
|
+
console.error(`📡 Polled E2E run status: ${updatedRun.status}`);
|
|
116
|
+
await onUpdate(updatedRun); // 🔁 Invoke the callback with the updated run
|
|
117
|
+
if (updatedRun.status === 'completed') {
|
|
118
|
+
clearInterval(interval);
|
|
119
|
+
clearTimeout(timeout);
|
|
120
|
+
await stopTunnel(`https://${e2eRun.key}.ngrok.debugg.ai`);
|
|
121
|
+
stopped = true;
|
|
122
|
+
}
|
|
123
|
+
}, 5000);
|
|
124
|
+
while (!stopped) {
|
|
125
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
126
|
+
}
|
|
127
|
+
return updatedRun;
|
|
128
|
+
}
|
|
129
|
+
async blockingHandleE2eRun(port, e2eRun) {
|
|
130
|
+
console.error(`🔧 Handling E2E run - ${e2eRun.uuid}`);
|
|
131
|
+
// Start ngrok tunnel
|
|
132
|
+
await startTunnel(port, `${e2eRun.key}.ngrok.debugg.ai`);
|
|
133
|
+
console.error(`🌐 Tunnel started at: ${e2eRun.key}.ngrok.debugg.ai`);
|
|
134
|
+
let stopped = false;
|
|
135
|
+
let lastStep = 0;
|
|
136
|
+
let updatedRun = e2eRun;
|
|
137
|
+
// Poll every second for completion
|
|
138
|
+
const interval = setInterval(async () => {
|
|
139
|
+
updatedRun = await this.client.e2es?.getE2eRun(e2eRun.uuid);
|
|
140
|
+
if (!updatedRun)
|
|
141
|
+
return;
|
|
142
|
+
console.error(`📡 Polled E2E run status: ${updatedRun.status}`);
|
|
143
|
+
if (updatedRun.status === 'completed') {
|
|
144
|
+
clearInterval(interval);
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
await stopTunnel(`https://${e2eRun.key}.ngrok.debugg.ai`);
|
|
147
|
+
// if (updatedRun.runGif) {
|
|
148
|
+
// fetchAndOpenGif(this.repoPath ?? "", updatedRun.runGif, updatedRun.test?.name ?? "", updatedRun.uuid);
|
|
149
|
+
// }
|
|
150
|
+
stopped = true;
|
|
151
|
+
}
|
|
152
|
+
}, 5000);
|
|
153
|
+
// Timeout safeguard
|
|
154
|
+
const timeout = setTimeout(async () => {
|
|
155
|
+
if (stopped)
|
|
156
|
+
return;
|
|
157
|
+
clearInterval(interval);
|
|
158
|
+
await stopTunnel(`https://${e2eRun.key}.ngrok.debugg.ai`);
|
|
159
|
+
console.error(`⏰ E2E test timed out after 15 minutes\n`);
|
|
160
|
+
stopped = true;
|
|
161
|
+
}, 900_000);
|
|
162
|
+
// Wait for the polling to complete or timeout to expire
|
|
163
|
+
while (!stopped) {
|
|
164
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
165
|
+
}
|
|
166
|
+
return updatedRun;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export default E2eTestRunner;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as http from "http";
|
|
3
|
+
import * as https from "https";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { URL } from "url";
|
|
6
|
+
export async function fetchAndOpenGif(projectRoot, recordingUrl, testName, testId) {
|
|
7
|
+
const cacheDir = path.join(projectRoot, ".debugg-ai", "e2e-runs");
|
|
8
|
+
console.error('....downloading gif....');
|
|
9
|
+
console.error('cacheDir', cacheDir);
|
|
10
|
+
console.error('testId', testId);
|
|
11
|
+
console.error('recordingUrl', recordingUrl);
|
|
12
|
+
let localUrl = recordingUrl.replace('localhost', 'localhost:8002');
|
|
13
|
+
console.error('localUrl', localUrl);
|
|
14
|
+
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
15
|
+
const filePath = path.join(cacheDir, `${testName.replace(/[^a-zA-Z0-9]/g, '-')}-${testId.slice(0, 4)}.gif`);
|
|
16
|
+
const fileUrl = new URL(localUrl);
|
|
17
|
+
const file = fs.createWriteStream(filePath);
|
|
18
|
+
console.error(`⬇️ Downloading test recording...`);
|
|
19
|
+
await new Promise((resolve, reject) => {
|
|
20
|
+
console.error('fetching gif', fileUrl);
|
|
21
|
+
if (fileUrl.protocol === 'https:') {
|
|
22
|
+
https.get(localUrl, (response) => {
|
|
23
|
+
if (response.statusCode !== 200) {
|
|
24
|
+
reject(new Error(`Failed to download file: ${response.statusCode}`));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
response.pipe(file);
|
|
28
|
+
file.on("finish", () => {
|
|
29
|
+
file.close();
|
|
30
|
+
resolve();
|
|
31
|
+
});
|
|
32
|
+
}).on("error", (err) => {
|
|
33
|
+
fs.unlinkSync(filePath);
|
|
34
|
+
reject(err);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
http.get(localUrl, (response) => {
|
|
39
|
+
if (response.statusCode !== 200) {
|
|
40
|
+
reject(new Error(`Failed to download file: ${response.statusCode}`));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
response.pipe(file);
|
|
44
|
+
file.on("finish", () => {
|
|
45
|
+
file.close();
|
|
46
|
+
resolve();
|
|
47
|
+
});
|
|
48
|
+
}).on("error", (err) => {
|
|
49
|
+
fs.unlinkSync(filePath);
|
|
50
|
+
reject(err);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
console.error(`📂 Opening test recording`);
|
|
55
|
+
// const fileUri = vscode.Uri.file(filePath);
|
|
56
|
+
// await vscode.commands.executeCommand('vscode.open', fileUri);
|
|
57
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export class RunResultFormatter {
|
|
2
|
+
steps = [];
|
|
3
|
+
passed(result) {
|
|
4
|
+
return result.status === "completed" && result.outcome === "pass";
|
|
5
|
+
}
|
|
6
|
+
formatFailures(result) {
|
|
7
|
+
if (this.passed(result) || !result.outcome)
|
|
8
|
+
return "";
|
|
9
|
+
return "\n\n❌ Failures:" + "\n" + `> ${result.outcome}`;
|
|
10
|
+
}
|
|
11
|
+
formatStepsAsMarkdown() {
|
|
12
|
+
if (this.steps.length === 0)
|
|
13
|
+
return "";
|
|
14
|
+
return ("\n\n" +
|
|
15
|
+
this.steps
|
|
16
|
+
.map((s, idx) => {
|
|
17
|
+
const num = `Step ${idx + 1}:`;
|
|
18
|
+
const label = s.label.padEnd(30);
|
|
19
|
+
const icon = "✅ Success";
|
|
20
|
+
// s.status === "pending"
|
|
21
|
+
// ? chalk.yellow("⏳ Pending")
|
|
22
|
+
// : s.status === "success"
|
|
23
|
+
// ? chalk.green("✅ Success")
|
|
24
|
+
// : chalk.red("❌ Failed");
|
|
25
|
+
return `${num} ${label} ${icon}`;
|
|
26
|
+
})
|
|
27
|
+
.join("\n\n"));
|
|
28
|
+
}
|
|
29
|
+
updateStep(label, status) {
|
|
30
|
+
const existing = this.steps.find((s) => s.label === label);
|
|
31
|
+
if (existing) {
|
|
32
|
+
existing.status = status;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.steps.push({ label, status });
|
|
36
|
+
}
|
|
37
|
+
console.error('updating step. steps ->', this.steps);
|
|
38
|
+
// Clear terminal and redraw
|
|
39
|
+
console.error("\x1Bc"); // ANSI clear screen
|
|
40
|
+
console.error("🧪 E2E Test Progress" +
|
|
41
|
+
`\r\n${this.steps
|
|
42
|
+
.map((s, i) => {
|
|
43
|
+
const icon = s.status === "pending"
|
|
44
|
+
? "⏳"
|
|
45
|
+
: s.status === "success"
|
|
46
|
+
? "✅"
|
|
47
|
+
: "❌";
|
|
48
|
+
return `${`Step ${i + 1}:`} ${s.label.padEnd(30)} ${icon}`;
|
|
49
|
+
})
|
|
50
|
+
.join("\r\n")}`);
|
|
51
|
+
}
|
|
52
|
+
formatTerminalBox(result) {
|
|
53
|
+
const header = this.passed(result)
|
|
54
|
+
? "✅ Test Passed"
|
|
55
|
+
: "❌ Test Failed";
|
|
56
|
+
const body = [
|
|
57
|
+
"Test: " + result.test?.name,
|
|
58
|
+
"Description: " + (result.test?.description ?? "None"),
|
|
59
|
+
"Duration: " + `${result.metrics?.executionTime ?? 0}s`,
|
|
60
|
+
"Status: " + result.status,
|
|
61
|
+
"Outcome: " + result.outcome,
|
|
62
|
+
this.formatStepsAsMarkdown(),
|
|
63
|
+
this.passed(result) ? "" : this.formatFailures(result),
|
|
64
|
+
]
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.join("\n");
|
|
67
|
+
return `${header}\n${body}`;
|
|
68
|
+
}
|
|
69
|
+
formatMarkdownSummary(result) {
|
|
70
|
+
return [
|
|
71
|
+
`🧪 **Test Name:** ${result.test?.name ?? "Unknown"}`,
|
|
72
|
+
`📄 **Description:** ${result.test?.description ?? "None"}`,
|
|
73
|
+
`⏱ **Duration:** ${result.metrics?.executionTime ?? 0}s`,
|
|
74
|
+
`🔎 **Status:** ${result.status}`,
|
|
75
|
+
`📊 **Outcome:** ${result.outcome}`,
|
|
76
|
+
this.formatStepsAsMarkdown(),
|
|
77
|
+
this.formatFailures(result),
|
|
78
|
+
]
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.join("\n")
|
|
81
|
+
.trim();
|
|
82
|
+
}
|
|
83
|
+
/*
|
|
84
|
+
Terminal uses different formatting than markdown.
|
|
85
|
+
*/
|
|
86
|
+
terminalSummary(result) {
|
|
87
|
+
return [
|
|
88
|
+
`🧪 Test Name: ${result.test?.name ?? "Unknown"}`,
|
|
89
|
+
`📄 Description: ${result.test?.description ?? "None"}`,
|
|
90
|
+
`⏱ Duration: ${result.metrics?.executionTime ?? 0}s`,
|
|
91
|
+
`🔎 Status: ${result.status}`,
|
|
92
|
+
`📊 Outcome: ${result.outcome}`,
|
|
93
|
+
this.formatFailures(result),
|
|
94
|
+
]
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.join("\r\n")
|
|
97
|
+
.trim();
|
|
98
|
+
}
|
|
99
|
+
appendToTestRun(result) {
|
|
100
|
+
console.error(this.terminalSummary(result));
|
|
101
|
+
}
|
|
102
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// index.ts
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { DebuggAIServerClient } from "./services/index.js";
|
|
6
|
+
import { E2eTestRunner } from "./e2e-agents/e2eRunner.js";
|
|
7
|
+
const createE2eTestTool = {
|
|
8
|
+
name: "debugg_ai_test_page_changes",
|
|
9
|
+
description: "Use DebuggAI to run & and test UI changes that have been made with its User emulation agents",
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
description: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Description of what page (relative url) and features should be tested.",
|
|
16
|
+
},
|
|
17
|
+
localPort: {
|
|
18
|
+
type: "number",
|
|
19
|
+
description: "Localhost port number where the app is running. Eg. 3000",
|
|
20
|
+
},
|
|
21
|
+
filePath: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Absolute path to the file to test",
|
|
24
|
+
},
|
|
25
|
+
repoName: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "The name of the current repository",
|
|
28
|
+
},
|
|
29
|
+
branchName: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Current branch name",
|
|
32
|
+
},
|
|
33
|
+
repoPath: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Local path of the repo root",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: ["description",],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
async function configureTestRunner(client) {
|
|
42
|
+
const e2eTestRunner = new E2eTestRunner(client);
|
|
43
|
+
return e2eTestRunner;
|
|
44
|
+
}
|
|
45
|
+
const server = new Server({
|
|
46
|
+
name: "DebuggAI MCP Server",
|
|
47
|
+
version: "0.1.0",
|
|
48
|
+
}, {
|
|
49
|
+
capabilities: {
|
|
50
|
+
tools: {},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
54
|
+
console.error("Received CallToolRequest:", req);
|
|
55
|
+
const apiKey = process.env.DEBUGGAI_API_KEY;
|
|
56
|
+
const testUsername = process.env.TEST_USERNAME_EMAIL;
|
|
57
|
+
const testPassword = process.env.TEST_USER_PASSWORD;
|
|
58
|
+
if (!apiKey || !testUsername || !testPassword) {
|
|
59
|
+
console.error("Missing one or more required environment variables: DEBUGGAI_API_KEY, TEST_USERNAME_EMAIL, TEST_USER_PASSWORD");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const { name, arguments: args } = req.params;
|
|
64
|
+
if (name === "debugg_ai_test_page_changes") {
|
|
65
|
+
const { description } = args;
|
|
66
|
+
let localPort = parseInt(process.env.DEBUGGAI_LOCAL_PORT ?? "3000");
|
|
67
|
+
let repoName = process.env.DEBUGGAI_LOCAL_REPO_NAME ?? "test-user-repo/test-repo";
|
|
68
|
+
let branchName = process.env.DEBUGGAI_LOCAL_BRANCH_NAME ?? "main";
|
|
69
|
+
let repoPath = process.env.DEBUGGAI_LOCAL_REPO_PATH ?? "/Users/test-user-repo/test-repo";
|
|
70
|
+
let filePath = process.env.DEBUGGAI_LOCAL_FILE_PATH ?? "/Users/test-user-repo/test-repo/index.ts";
|
|
71
|
+
const progressToken = req.params._meta?.progressToken;
|
|
72
|
+
if (args?.localPort) {
|
|
73
|
+
localPort = args.localPort;
|
|
74
|
+
}
|
|
75
|
+
if (args?.repoName) {
|
|
76
|
+
repoName = args.repoName;
|
|
77
|
+
}
|
|
78
|
+
if (args?.branchName) {
|
|
79
|
+
branchName = args.branchName;
|
|
80
|
+
}
|
|
81
|
+
if (args?.repoPath) {
|
|
82
|
+
repoPath = args.repoPath;
|
|
83
|
+
}
|
|
84
|
+
if (args?.filePath) {
|
|
85
|
+
filePath = args.filePath;
|
|
86
|
+
}
|
|
87
|
+
if (progressToken == undefined) {
|
|
88
|
+
console.error("No progress token found");
|
|
89
|
+
return {
|
|
90
|
+
content: [
|
|
91
|
+
{ type: "text", text: "No progress token found" },
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const client = new DebuggAIServerClient(process.env.DEBUGGAI_API_KEY ?? "");
|
|
96
|
+
const e2eTestRunner = await configureTestRunner(client);
|
|
97
|
+
const e2eRun = await e2eTestRunner.createNewE2eTest(localPort, description, repoName, branchName, repoPath, filePath);
|
|
98
|
+
if (!e2eRun) {
|
|
99
|
+
console.error("Failed to create E2E test");
|
|
100
|
+
return {
|
|
101
|
+
content: [
|
|
102
|
+
{ type: "text", text: "Failed to create E2E test" },
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
await server.notification({
|
|
107
|
+
method: "notifications/progress",
|
|
108
|
+
params: {
|
|
109
|
+
progress: 0,
|
|
110
|
+
total: 20,
|
|
111
|
+
progressToken,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
const finalRun = await e2eTestRunner.handleE2eRun(e2eRun, async (update) => {
|
|
115
|
+
console.error(`📢 Status: ${update.status}`);
|
|
116
|
+
const curStep = update.conversations?.[0]?.messages?.length;
|
|
117
|
+
const totalSteps = update.conversations?.[0]?.messages?.length;
|
|
118
|
+
await server.notification({
|
|
119
|
+
method: "notifications/progress",
|
|
120
|
+
params: {
|
|
121
|
+
progress: curStep,
|
|
122
|
+
total: 20,
|
|
123
|
+
progressToken,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
const testOutcome = finalRun?.outcome;
|
|
128
|
+
const testDetails = finalRun?.conversations?.[0]?.messages?.map((message) => message.jsonContent?.currentState?.nextGoal);
|
|
129
|
+
const runGif = finalRun?.runGif;
|
|
130
|
+
let base64 = "";
|
|
131
|
+
if (runGif) {
|
|
132
|
+
const response = await fetch(runGif);
|
|
133
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
134
|
+
base64 = Buffer.from(arrayBuffer).toString('base64');
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: "text",
|
|
140
|
+
text: JSON.stringify({ testOutcome, testDetails }, null, 2),
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: "image",
|
|
144
|
+
data: base64,
|
|
145
|
+
mimeType: "image/jpeg",
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`Tool not found: ${name}`);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
return {
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: "text",
|
|
157
|
+
text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }),
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
164
|
+
return {
|
|
165
|
+
tools: [createE2eTestTool],
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
async function main() {
|
|
169
|
+
console.error("Starting DebuggAI MCP server...");
|
|
170
|
+
const transport = new StdioServerTransport();
|
|
171
|
+
await server.connect(transport);
|
|
172
|
+
console.error("DebuggAI MCP Server running on stdio");
|
|
173
|
+
}
|
|
174
|
+
main().catch((err) => {
|
|
175
|
+
console.error("Fatal error in main():", err);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export const createCoverageService = (tx) => ({
|
|
2
|
+
/**
|
|
3
|
+
* Create a test coverage file for a given file
|
|
4
|
+
*/
|
|
5
|
+
async createCoverage(fileContents, filePath, repoName, branchName, params) {
|
|
6
|
+
try {
|
|
7
|
+
const serverUrl = "api/v1/coverage/";
|
|
8
|
+
console.error('Branch name - ', branchName, ' repo name - ', repoName, ' repo path - ', params?.repoPath);
|
|
9
|
+
let relativePath = filePath;
|
|
10
|
+
// Convert absolute path to relative path
|
|
11
|
+
if (params?.repoPath) {
|
|
12
|
+
relativePath = filePath.replace(params?.repoPath + "/", "");
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
console.error("No repo path found for file");
|
|
16
|
+
// split based on the repo name
|
|
17
|
+
const repoBaseName = repoName.split("/")[-1]; // typically the form of 'userName/repoName'
|
|
18
|
+
const splitPath = filePath.split(repoBaseName);
|
|
19
|
+
if (splitPath.length === 2) { // if the repo name is in the path & only once, otherwise unclear how to handle
|
|
20
|
+
relativePath = splitPath[1];
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
relativePath = filePath;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
console.error("GET_COVERAGE: Full path - ", filePath, ". Relative path - ", relativePath);
|
|
27
|
+
const fileParams = {
|
|
28
|
+
...params,
|
|
29
|
+
fileContents: fileContents,
|
|
30
|
+
absPath: filePath,
|
|
31
|
+
filePath: relativePath,
|
|
32
|
+
repoName: repoName,
|
|
33
|
+
branchName: branchName,
|
|
34
|
+
};
|
|
35
|
+
const response = await tx.post(serverUrl, { ...fileParams });
|
|
36
|
+
console.error("Raw API response:", response);
|
|
37
|
+
return response;
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
console.error("Error fetching issues in file:", err);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* Log a failed run for a given test file
|
|
46
|
+
*/
|
|
47
|
+
async logFailedRun(fileContents, filePath, repoName, branchName, params) {
|
|
48
|
+
try {
|
|
49
|
+
const serverUrl = "api/v1/coverage/log_failed_run/";
|
|
50
|
+
console.error('Branch name - ', branchName, ' repo name - ', repoName, ' repo path - ', params?.repoPath);
|
|
51
|
+
let relativePath = filePath;
|
|
52
|
+
// Convert absolute path to relative path
|
|
53
|
+
if (params?.repoPath) {
|
|
54
|
+
relativePath = filePath.replace(params?.repoPath + "/", "");
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.error("No repo path found for file");
|
|
58
|
+
// split based on the repo name
|
|
59
|
+
const repoBaseName = repoName.split("/")[-1]; // typically the form of 'userName/repoName'
|
|
60
|
+
const splitPath = filePath.split(repoBaseName);
|
|
61
|
+
if (splitPath.length === 2) { // if the repo name is in the path & only once, otherwise unclear how to handle
|
|
62
|
+
relativePath = splitPath[1];
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
relativePath = filePath;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
console.error("GET_COVERAGE: Full path - ", filePath, ". Relative path - ", relativePath);
|
|
69
|
+
const fileParams = {
|
|
70
|
+
...params,
|
|
71
|
+
fileContents: fileContents,
|
|
72
|
+
absPath: filePath,
|
|
73
|
+
filePath: relativePath,
|
|
74
|
+
repoName: repoName,
|
|
75
|
+
branchName: branchName,
|
|
76
|
+
};
|
|
77
|
+
const response = await tx.post(serverUrl, { ...fileParams });
|
|
78
|
+
console.error("Raw API response:", response);
|
|
79
|
+
return response;
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
console.error("Error fetching issues in file:", err);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
/**
|
|
87
|
+
* Get a test coverage file for a given file
|
|
88
|
+
*/
|
|
89
|
+
async getCoverage(filePath, repoName, branchName, params) {
|
|
90
|
+
try {
|
|
91
|
+
const serverUrl = "api/v1/coverage/for_file/";
|
|
92
|
+
console.error('Branch name - ', branchName, ' repo name - ', repoName, ' repo path - ', params?.repoPath);
|
|
93
|
+
let relativePath = filePath;
|
|
94
|
+
// Convert absolute path to relative path
|
|
95
|
+
if (params?.repoPath) {
|
|
96
|
+
relativePath = filePath.replace(params?.repoPath + "/", "");
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.error("No repo path found for file");
|
|
100
|
+
// split based on the repo name
|
|
101
|
+
const repoBaseName = repoName.split("/")[-1]; // typically the form of 'userName/repoName'
|
|
102
|
+
const splitPath = filePath.split(repoBaseName);
|
|
103
|
+
if (splitPath.length === 2) { // if the repo name is in the path & only once, otherwise unclear how to handle
|
|
104
|
+
relativePath = splitPath[1];
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
relativePath = filePath;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
console.error("GET_COVERAGE: Full path - ", filePath, ". Relative path - ", relativePath);
|
|
111
|
+
const fileParams = {
|
|
112
|
+
...params,
|
|
113
|
+
filePath: relativePath,
|
|
114
|
+
absPath: filePath,
|
|
115
|
+
repoName: repoName,
|
|
116
|
+
branchName: branchName,
|
|
117
|
+
};
|
|
118
|
+
const response = await tx.get(serverUrl, { ...fileParams });
|
|
119
|
+
console.error("Raw API response:", response);
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
console.error("Error fetching issues in file:", err);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|