@debugg-ai/debugg-ai-mcp 1.0.2 → 1.0.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.
@@ -7,8 +7,14 @@ async function startTunnel(localPort, domain) {
7
7
  // addr: localPort,
8
8
  // hostname: domain,
9
9
  // });
10
- const url = await ngrok.connect({ addr: localPort, hostname: domain });
11
- return url;
10
+ if (process.env.DOCKER_CONTAINER === "true") {
11
+ const url = await ngrok.connect({ addr: `host.docker.internal:${localPort}`, hostname: domain });
12
+ return url;
13
+ }
14
+ else {
15
+ const url = await ngrok.connect({ addr: localPort, hostname: domain });
16
+ return url;
17
+ }
12
18
  }
13
19
  catch (err) {
14
20
  console.error('Error starting ngrok tunnel:', err);
@@ -31,6 +31,6 @@ export class DebuggAIServerClient {
31
31
  * @returns The server URL
32
32
  */
33
33
  async getServerUrl() {
34
- return "http://localhost:8002";
34
+ return "https://debuggai-backend.ngrok.app";
35
35
  }
36
36
  }
@@ -1,4 +1,4 @@
1
- import { objToCamelCase, objToSnakeCase } from "../utils/objectNaming.js";
1
+ import { objToCamelCase, objToSnakeCase } from "./objectNaming.js";
2
2
  import { destroy as destroyAxios, get as getAxios, post as postAxios, put as putAxios } from "./axios.js";
3
3
  export async function get(url, params) {
4
4
  const fmtdParams = objToSnakeCase(params);
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@debugg-ai/debugg-ai-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "MCP Server for debugg ai web browsing",
5
5
  "type": "module",
6
6
  "bin": {
7
- "debugg-ai-mcp": "dist/index.js"
7
+ "@debugg-ai/debugg-ai-mcp": "dist/index.js"
8
8
  },
9
9
  "files": [
10
10
  "dist"
11
11
  ],
12
12
  "scripts": {
13
- "build": "tsc && shx chmod +x dist/*.js",
13
+ "build": "tsc && shx chmod +x dist/index.js",
14
14
  "prepare": "npm run build",
15
15
  "watch": "tsc --watch"
16
16
  },
@@ -1,127 +0,0 @@
1
- import { downloadBinary, start, stop } from '../tunnels/ngrok/index.js';
2
- async function startTunnel(localPort, domain) {
3
- try {
4
- await start({
5
- addr: localPort,
6
- hostname: domain,
7
- onLogEvent: (data) => {
8
- console.error(`${localPort} | ${domain} | ngrok log: ${data}`);
9
- },
10
- });
11
- return domain;
12
- }
13
- catch (err) {
14
- console.error('Error starting ngrok tunnel:', err);
15
- }
16
- }
17
- export class E2eTestRunner {
18
- client;
19
- constructor(client) {
20
- this.client = client;
21
- this.setup();
22
- }
23
- async setup() {
24
- await this.configureNgrok();
25
- }
26
- async configureNgrok() {
27
- await downloadBinary();
28
- }
29
- async startTunnel(port, url) {
30
- await startTunnel(port, url);
31
- console.error(`Tunnel started at: ${url}`);
32
- return url;
33
- }
34
- /**
35
- * Run E2E test generator for a single file *quietly* in the background.
36
- * @param filePath absolute path of the file to test
37
- */
38
- async runTests(e2eRun) {
39
- // Start by opening an ngrok tunnel.
40
- // call the debugg ai endpoint to start running the test
41
- // retrieve the results when done
42
- // save files locally somewhere
43
- const listener = await startTunnel(3011, `${e2eRun.key}.ngrok.debugg.ai`);
44
- console.error(`Tunnel started at: ${listener}`);
45
- const interval = setInterval(async () => {
46
- const newE2eRun = await this.client.e2es?.getE2eRun(e2eRun.id);
47
- console.error(`E2E run - ${newE2eRun}`);
48
- if (newE2eRun?.status === 'completed') {
49
- console.error(`E2E run completed - ${newE2eRun}`);
50
- clearInterval(interval);
51
- await stop(listener);
52
- }
53
- }, 1000);
54
- // if the run doesn't complete in time, disconnect the tunnel
55
- const setTimer = setTimeout(async () => {
56
- clearInterval(interval);
57
- clearTimeout(setTimer);
58
- await stop(listener);
59
- }, 300000);
60
- return undefined;
61
- }
62
- /**
63
- * Create a new E2E test and run it.
64
- * @param testPort - The port to use for the test.
65
- * @param testDescription - The description of the test.
66
- * @param filePath - The path to the file to test.
67
- * @param repoName - The name of the repository.
68
- * @param branchName - The name of the branch.
69
- * @param repoPath - The path to the repository.
70
- */
71
- async createNewE2eTest(testPort, testDescription, repoName, branchName, repoPath, filePath) {
72
- console.error(`Creating new E2E test with description: ${testDescription}`);
73
- const e2eTest = await this.client.e2es?.createE2eTest(testDescription, filePath ?? "", repoName, branchName, {
74
- repoPath: repoPath
75
- });
76
- console.error(`E2E test created - ${e2eTest}`);
77
- if (!e2eTest) {
78
- console.error("Failed to create E2E test.");
79
- return null;
80
- }
81
- if (!e2eTest.curRun) {
82
- console.error("Failed to create E2E test run.");
83
- return null;
84
- }
85
- return this.handleE2eRun(testPort, e2eTest.curRun);
86
- }
87
- async handleE2eRun(port, e2eRun) {
88
- console.error(`🔧 Handling E2E run - ${e2eRun.uuid}`);
89
- // Start ngrok tunnel
90
- await startTunnel(port, `${e2eRun.key}.ngrok.debugg.ai`);
91
- console.error(`🌐 Tunnel started at: ${e2eRun.key}.ngrok.debugg.ai`);
92
- let stopped = false;
93
- let lastStep = 0;
94
- let updatedRun = e2eRun;
95
- // Poll every second for completion
96
- const interval = setInterval(async () => {
97
- updatedRun = await this.client.e2es?.getE2eRun(e2eRun.id);
98
- if (!updatedRun)
99
- return;
100
- console.error(`📡 Polled E2E run status: ${updatedRun.status}`);
101
- if (updatedRun.status === 'completed') {
102
- clearInterval(interval);
103
- clearTimeout(timeout);
104
- await stop(`https://${e2eRun.key}.ngrok.debugg.ai`);
105
- // if (updatedRun.runGif) {
106
- // fetchAndOpenGif(this.repoPath ?? "", updatedRun.runGif, updatedRun.test?.name ?? "", updatedRun.uuid);
107
- // }
108
- stopped = true;
109
- }
110
- }, 5000);
111
- // Timeout safeguard
112
- const timeout = setTimeout(async () => {
113
- if (stopped)
114
- return;
115
- clearInterval(interval);
116
- await stop(`https://${e2eRun.key}.ngrok.debugg.ai`);
117
- console.error(`⏰ E2E test timed out after 15 minutes\n`);
118
- stopped = true;
119
- }, 900_000);
120
- // Wait for the polling to complete or timeout to expire
121
- while (!stopped) {
122
- await new Promise(resolve => setTimeout(resolve, 1000));
123
- }
124
- return updatedRun;
125
- }
126
- }
127
- export default E2eTestRunner;
@@ -1,57 +0,0 @@
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
- }
@@ -1,102 +0,0 @@
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/src/index.js DELETED
@@ -1,107 +0,0 @@
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: ["localPort", "description", "repoPath", "repoName",],
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
- const client = new DebuggAIServerClient(apiKey);
65
- if (name === "debugg_ai_create_new_e2e_test") {
66
- const { description, localPort, repoName, branchName, repoPath, filePath } = args;
67
- const e2eTestRunner = await configureTestRunner(client);
68
- const test = await e2eTestRunner.createNewE2eTest(localPort, description, repoName, branchName, repoPath, filePath);
69
- const testOutcome = test?.outcome;
70
- const testDetails = test?.conversations?.[0]?.messages?.map((message) => message.content);
71
- return {
72
- content: [
73
- {
74
- type: "text",
75
- text: JSON.stringify({ testOutcome, testDetails }, null, 2),
76
- },
77
- ],
78
- };
79
- }
80
- throw new Error(`Tool not found: ${name}`);
81
- }
82
- catch (err) {
83
- return {
84
- content: [
85
- {
86
- type: "text",
87
- text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }),
88
- },
89
- ],
90
- };
91
- }
92
- });
93
- server.setRequestHandler(ListToolsRequestSchema, async () => {
94
- return {
95
- tools: [createE2eTestTool],
96
- };
97
- });
98
- async function main() {
99
- console.error("Starting DebuggAI MCP server...");
100
- const transport = new StdioServerTransport();
101
- await server.connect(transport);
102
- console.error("DebuggAI MCP Server running on stdio");
103
- }
104
- main().catch((err) => {
105
- console.error("Fatal error in main():", err);
106
- process.exit(1);
107
- });
@@ -1,127 +0,0 @@
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
- });