@debugg-ai/debugg-ai-mcp 1.0.45 → 1.0.47
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.
|
@@ -132,72 +132,69 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
132
132
|
const executionUuid = executeResponse.executionUuid;
|
|
133
133
|
logger.info(`Execution queued: ${executionUuid}`);
|
|
134
134
|
// --- Poll ---
|
|
135
|
-
//
|
|
136
|
-
//
|
|
135
|
+
// Progress phases:
|
|
136
|
+
// 1-3: MCP setup (tunnel, template, queue) — already sent above
|
|
137
|
+
// 4-6: Backend setup (trigger, browser.setup, subworkflow starting)
|
|
138
|
+
// 7-27: Agent steps (mapped from state.stepsTaken)
|
|
139
|
+
// 28: Complete
|
|
140
|
+
const BACKEND_SETUP_END = 6;
|
|
137
141
|
let lastStepsTaken = 0;
|
|
138
|
-
let lastNodeCount = 0;
|
|
139
142
|
let observedMaxSteps = MAX_EXEC_STEPS;
|
|
140
143
|
const finalExecution = await client.workflows.pollExecution(executionUuid, async (exec) => {
|
|
141
144
|
// Keep the tunnel alive while the workflow is actively running
|
|
142
145
|
if (ctx.tunnelId)
|
|
143
146
|
touchTunnelById(ctx.tunnelId);
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
// Prefer actual brain.step node count over API stepsTaken (which may lag)
|
|
148
|
-
const stepsTaken = Math.max(brainStepCount, exec.state?.stepsTaken ?? 0);
|
|
149
|
-
if (nodeCount !== lastNodeCount || stepsTaken !== lastStepsTaken || exec.status !== 'pending') {
|
|
150
|
-
lastNodeCount = nodeCount;
|
|
147
|
+
const nodes = exec.nodeExecutions ?? [];
|
|
148
|
+
const stepsTaken = Math.max(nodes.filter(n => n.nodeType === 'brain.step').length, exec.state?.stepsTaken ?? 0);
|
|
149
|
+
if (stepsTaken !== lastStepsTaken) {
|
|
151
150
|
lastStepsTaken = stepsTaken;
|
|
152
|
-
logger.info(`Execution status: ${exec.status}, nodes: ${
|
|
151
|
+
logger.info(`Execution status: ${exec.status}, nodes: ${nodes.length}, steps: ${stepsTaken}`);
|
|
153
152
|
}
|
|
154
|
-
if (progressCallback)
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
if (!progressCallback)
|
|
154
|
+
return;
|
|
155
|
+
// --- Compute progress number ---
|
|
156
|
+
let execProgress;
|
|
157
|
+
let message;
|
|
158
|
+
if (stepsTaken > 0) {
|
|
159
|
+
// Agent is actively stepping — map into slots 7..27
|
|
160
|
+
if (stepsTaken > observedMaxSteps)
|
|
157
161
|
observedMaxSteps = stepsTaken + 5;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
const stepSlots = TOTAL_STEPS - BACKEND_SETUP_END - 1; // 21 slots
|
|
163
|
+
execProgress = BACKEND_SETUP_END + Math.max(1, Math.round((stepsTaken / observedMaxSteps) * stepSlots));
|
|
164
|
+
execProgress = Math.min(execProgress, TOTAL_STEPS - 1);
|
|
165
|
+
// Use state.currentAction for the message (backend sends intent + actionType)
|
|
166
|
+
const ca = exec.state?.currentAction;
|
|
167
|
+
if (ca?.intent) {
|
|
168
|
+
const action = ca.actionType ?? ca.action_type ?? 'working';
|
|
169
|
+
message = `Step ${stepsTaken}: [${action}] ${ca.intent}`;
|
|
164
170
|
}
|
|
165
171
|
else {
|
|
166
|
-
|
|
167
|
-
execProgress = SETUP_STEPS + 1;
|
|
172
|
+
message = `Agent evaluating... (step ${stepsTaken})`;
|
|
168
173
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
message = `Agent evaluating app... (step ${stepsTaken})`;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
else if (nodeCount === 0) {
|
|
190
|
-
message = 'Browser agent starting up...';
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
message = 'Browser ready, agent navigating...';
|
|
194
|
-
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// No agent steps yet — show backend setup progress from node transitions
|
|
177
|
+
const hasSubworkflow = nodes.some(n => n.nodeType === 'subworkflow.run');
|
|
178
|
+
const hasBrowserSetup = nodes.some(n => n.nodeType === 'browser.setup');
|
|
179
|
+
const browserReady = nodes.some(n => n.nodeType === 'browser.setup' && n.status === 'success');
|
|
180
|
+
if (browserReady || hasSubworkflow) {
|
|
181
|
+
execProgress = BACKEND_SETUP_END;
|
|
182
|
+
message = 'Browser ready, agent starting...';
|
|
183
|
+
}
|
|
184
|
+
else if (hasBrowserSetup) {
|
|
185
|
+
execProgress = SETUP_STEPS + 2;
|
|
186
|
+
message = 'Launching browser...';
|
|
187
|
+
}
|
|
188
|
+
else if (nodes.length > 0) {
|
|
189
|
+
execProgress = SETUP_STEPS + 1;
|
|
190
|
+
message = 'Workflow triggered, preparing...';
|
|
195
191
|
}
|
|
196
192
|
else {
|
|
197
|
-
|
|
193
|
+
execProgress = SETUP_STEPS + 1;
|
|
194
|
+
message = 'Waiting for execution to start...';
|
|
198
195
|
}
|
|
199
|
-
await progressCallback({ progress: execProgress, total: TOTAL_STEPS, message });
|
|
200
196
|
}
|
|
197
|
+
await progressCallback({ progress: execProgress, total: TOTAL_STEPS, message });
|
|
201
198
|
}, abortController.signal);
|
|
202
199
|
const duration = Date.now() - startTime;
|
|
203
200
|
// --- Format result ---
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,8 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
20
20
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
21
21
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
22
22
|
import { config } from "./config/index.js";
|
|
23
|
-
import {
|
|
23
|
+
import { initTools, getTools, getTool } from "./tools/index.js";
|
|
24
|
+
import { resolveProjectContext } from "./services/projectContext.js";
|
|
24
25
|
import { Logger, validateInput, createErrorResponse, toMCPError, handleConfigurationError, Telemetry, TelemetryEvents, } from "./utils/index.js";
|
|
25
26
|
// Initialize logger
|
|
26
27
|
const logger = new Logger({ module: 'main' });
|
|
@@ -132,6 +133,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
132
133
|
* Handle list tools requests
|
|
133
134
|
*/
|
|
134
135
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
136
|
+
const tools = getTools();
|
|
135
137
|
logger.info('Tools list requested', { toolCount: tools.length });
|
|
136
138
|
return {
|
|
137
139
|
tools: tools,
|
|
@@ -162,12 +164,18 @@ async function main() {
|
|
|
162
164
|
}));
|
|
163
165
|
logger.info('Telemetry enabled (PostHog)');
|
|
164
166
|
}
|
|
167
|
+
// Resolve project context (repo → project → environments/credentials)
|
|
168
|
+
// This enriches tool descriptions with available credentials for the detected project.
|
|
169
|
+
const projectCtx = await resolveProjectContext();
|
|
170
|
+
initTools(projectCtx);
|
|
165
171
|
// Create and connect transport
|
|
166
172
|
const transport = new StdioServerTransport();
|
|
167
173
|
await server.connect(transport);
|
|
174
|
+
const tools = getTools();
|
|
168
175
|
logger.info('DebuggAI MCP Server is running and ready to accept requests', {
|
|
169
176
|
transport: 'stdio',
|
|
170
|
-
toolsAvailable: tools.map(t => t.name)
|
|
177
|
+
toolsAvailable: tools.map(t => t.name),
|
|
178
|
+
detectedProject: projectCtx?.project.name ?? null,
|
|
171
179
|
});
|
|
172
180
|
}
|
|
173
181
|
catch (error) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Context Service
|
|
3
|
+
* At startup: detect repo → resolve project → fetch environments + credentials.
|
|
4
|
+
* Exposes the result so tool descriptions can be enriched dynamically.
|
|
5
|
+
*/
|
|
6
|
+
import { config } from '../config/index.js';
|
|
7
|
+
import { DebuggAIServerClient } from './index.js';
|
|
8
|
+
import { detectRepoName } from '../utils/gitContext.js';
|
|
9
|
+
import { Logger } from '../utils/logger.js';
|
|
10
|
+
const logger = new Logger({ module: 'projectContext' });
|
|
11
|
+
let cached = null;
|
|
12
|
+
let initialized = false;
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the current project context: repo → project → environments → credentials.
|
|
15
|
+
* Safe to call multiple times — caches after first successful resolution.
|
|
16
|
+
*/
|
|
17
|
+
export async function resolveProjectContext() {
|
|
18
|
+
if (initialized)
|
|
19
|
+
return cached;
|
|
20
|
+
initialized = true;
|
|
21
|
+
const repoName = detectRepoName();
|
|
22
|
+
if (!repoName) {
|
|
23
|
+
logger.info('No git repo detected — skipping project context');
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const client = new DebuggAIServerClient(config.api.key);
|
|
28
|
+
await client.init();
|
|
29
|
+
const project = await client.findProjectByRepoName(repoName);
|
|
30
|
+
if (!project) {
|
|
31
|
+
logger.info(`No project found for repo "${repoName}"`);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
logger.info(`Resolved project: ${project.name} (${project.uuid})`);
|
|
35
|
+
// Fetch environments for this project
|
|
36
|
+
const envResponse = await client.tx.get(`api/v1/projects/${project.uuid}/environments/`);
|
|
37
|
+
const rawEnvs = envResponse?.results ?? [];
|
|
38
|
+
// Fetch credentials for each environment in parallel
|
|
39
|
+
const environments = await Promise.all(rawEnvs
|
|
40
|
+
.filter((e) => e.isActive)
|
|
41
|
+
.map(async (env) => {
|
|
42
|
+
let credentials = [];
|
|
43
|
+
try {
|
|
44
|
+
const credResponse = await client.tx.get(`api/v1/projects/${project.uuid}/environments/${env.uuid}/credentials/`);
|
|
45
|
+
credentials = (credResponse?.results ?? [])
|
|
46
|
+
.filter((c) => c.isActive)
|
|
47
|
+
.map((c) => ({
|
|
48
|
+
uuid: c.uuid,
|
|
49
|
+
label: c.label || c.username,
|
|
50
|
+
username: c.username,
|
|
51
|
+
role: c.role,
|
|
52
|
+
environmentName: env.name,
|
|
53
|
+
environmentUuid: env.uuid,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Some environments may not support credentials
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
uuid: env.uuid,
|
|
61
|
+
name: env.name,
|
|
62
|
+
url: env.url || env.activeUrl || '',
|
|
63
|
+
credentials,
|
|
64
|
+
};
|
|
65
|
+
}));
|
|
66
|
+
cached = { repoName, project, environments };
|
|
67
|
+
const totalCreds = environments.reduce((n, e) => n + e.credentials.length, 0);
|
|
68
|
+
logger.info(`Project context ready: ${environments.length} environments, ${totalCreds} credentials`);
|
|
69
|
+
return cached;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
logger.warn(`Failed to resolve project context: ${err}`);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function getProjectContext() {
|
|
77
|
+
return cached;
|
|
78
|
+
}
|
|
@@ -23,6 +23,10 @@ export const createWorkflowsService = (tx) => {
|
|
|
23
23
|
},
|
|
24
24
|
async executeWorkflow(workflowUuid, contextData, env) {
|
|
25
25
|
const body = { contextData };
|
|
26
|
+
// Send projectId at top level too — backend may read it from either location
|
|
27
|
+
if (contextData.projectId) {
|
|
28
|
+
body.projectId = contextData.projectId;
|
|
29
|
+
}
|
|
26
30
|
if (env && Object.keys(env).length > 0) {
|
|
27
31
|
body.env = env;
|
|
28
32
|
}
|
package/dist/tools/index.js
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { buildTestPageChangesTool, buildValidatedTestPageChangesTool } from './testPageChanges.js';
|
|
2
|
+
let _tools = null;
|
|
3
|
+
let _validatedTools = null;
|
|
4
|
+
const toolRegistry = new Map();
|
|
5
|
+
/**
|
|
6
|
+
* Initialize tools with project context (call once after resolveProjectContext).
|
|
7
|
+
*/
|
|
8
|
+
export function initTools(ctx) {
|
|
9
|
+
const tool = buildTestPageChangesTool(ctx);
|
|
10
|
+
const validated = buildValidatedTestPageChangesTool(ctx);
|
|
11
|
+
_tools = [tool];
|
|
12
|
+
_validatedTools = [validated];
|
|
13
|
+
toolRegistry.clear();
|
|
14
|
+
toolRegistry.set(validated.name, validated);
|
|
15
|
+
}
|
|
16
|
+
export function getTools() {
|
|
17
|
+
if (!_tools)
|
|
18
|
+
initTools(null);
|
|
19
|
+
return _tools;
|
|
11
20
|
}
|
|
12
21
|
export function getTool(name) {
|
|
22
|
+
if (!_validatedTools)
|
|
23
|
+
initTools(null);
|
|
13
24
|
return toolRegistry.get(name);
|
|
14
25
|
}
|
|
15
|
-
export function hasToolTool(name) {
|
|
16
|
-
return toolRegistry.has(name);
|
|
17
|
-
}
|
|
@@ -1,62 +1,97 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Test Page Changes Tool Definition
|
|
3
|
-
* Defines the check_app_in_browser tool with proper validation
|
|
3
|
+
* Defines the check_app_in_browser tool with proper validation.
|
|
4
|
+
* Tool description is enriched at startup with available environments/credentials.
|
|
4
5
|
*/
|
|
5
6
|
import { TestPageChangesInputSchema } from '../types/index.js';
|
|
6
7
|
import { testPageChangesHandler } from '../handlers/testPageChangesHandler.js';
|
|
8
|
+
const BASE_DESCRIPTION = `Give an AI agent eyes on a live website or app. The agent browses it, interacts with it, and tells you whether a given task or check passed. Works on localhost or any URL. Use for visual QA, flow validation, regression checks, or anything that needs a real browser to verify.
|
|
9
|
+
|
|
10
|
+
LOCALHOST SUPPORT: Pass any localhost URL (e.g. http://localhost:3000) and it Just Works. A secure tunnel is automatically created so the remote browser can reach your local dev server — no manual ngrok setup, no port forwarding, no config.`;
|
|
7
11
|
/**
|
|
8
|
-
*
|
|
12
|
+
* Build the dynamic tool description including available environments/credentials.
|
|
9
13
|
*/
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
description:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
14
|
+
export function buildToolDescription(ctx) {
|
|
15
|
+
if (!ctx)
|
|
16
|
+
return BASE_DESCRIPTION;
|
|
17
|
+
const envsWithCreds = ctx.environments.filter(e => e.credentials.length > 0);
|
|
18
|
+
if (envsWithCreds.length === 0) {
|
|
19
|
+
return `${BASE_DESCRIPTION}\n\nDETECTED PROJECT: "${ctx.project.name}" (repo: ${ctx.repoName}). No credentials configured — provide username/password if the app requires login.`;
|
|
20
|
+
}
|
|
21
|
+
const lines = [
|
|
22
|
+
`\n\nDETECTED PROJECT: "${ctx.project.name}" (repo: ${ctx.repoName})`,
|
|
23
|
+
`\nAVAILABLE ENVIRONMENTS & CREDENTIALS (pass environmentId + credentialId for authenticated testing):`,
|
|
24
|
+
];
|
|
25
|
+
for (const env of envsWithCreds) {
|
|
26
|
+
lines.push(`\n Environment: "${env.name}" (${env.uuid})${env.url ? ` — ${env.url}` : ''}`);
|
|
27
|
+
for (const cred of env.credentials) {
|
|
28
|
+
const parts = [` - "${cred.label}" (${cred.uuid}) — user: ${cred.username}`];
|
|
29
|
+
if (cred.role)
|
|
30
|
+
parts[0] += `, role: ${cred.role}`;
|
|
31
|
+
lines.push(parts[0]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
lines.push(`\nTo use: pass environmentId and credentialId from above. Or provide username/password directly.`);
|
|
35
|
+
return BASE_DESCRIPTION + lines.join('\n');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build the full tool definition, optionally enriched with project context.
|
|
39
|
+
*/
|
|
40
|
+
export function buildTestPageChangesTool(ctx) {
|
|
41
|
+
return {
|
|
42
|
+
name: "check_app_in_browser",
|
|
43
|
+
title: "Run E2E Browser Test",
|
|
44
|
+
description: buildToolDescription(ctx),
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
description: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "Natural language description of what to test or evaluate (e.g., 'Does the login form validate empty fields?' or 'Navigate to the homepage and verify the hero section loads')",
|
|
51
|
+
minLength: 1
|
|
52
|
+
},
|
|
53
|
+
url: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "URL to navigate to. Can be any public URL (https://example.com) OR a localhost/local dev server URL. For localhost URLs, a secure tunnel is automatically created — just make sure your dev server is running on that port."
|
|
56
|
+
},
|
|
57
|
+
environmentId: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "UUID of a specific environment to use for this test. See available environments in the tool description above."
|
|
60
|
+
},
|
|
61
|
+
credentialId: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "UUID of a specific credential to use for login. See available credentials in the tool description above."
|
|
64
|
+
},
|
|
65
|
+
credentialRole: {
|
|
66
|
+
type: "string",
|
|
67
|
+
description: "Pick a credential by role (e.g. 'admin', 'guest') from the resolved environment"
|
|
68
|
+
},
|
|
69
|
+
username: {
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "A real, existing account email for the target app. Do NOT invent or guess credentials — use one from the available credentials listed above, or ask the user. The browser agent will type this into the login form."
|
|
72
|
+
},
|
|
73
|
+
password: {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "The real password for the username above. Do NOT guess or use placeholder passwords — use credentials from the list above or ask the user."
|
|
76
|
+
},
|
|
77
|
+
repoName: {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "GitHub repository name (e.g. 'my-org/my-repo'). Auto-detected from the current git repo — only provide this if you want to run against a different project than the one you're in."
|
|
80
|
+
},
|
|
49
81
|
},
|
|
82
|
+
required: ["description", "url"],
|
|
83
|
+
additionalProperties: false
|
|
50
84
|
},
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
},
|
|
54
|
-
};
|
|
85
|
+
};
|
|
86
|
+
}
|
|
55
87
|
/**
|
|
56
|
-
*
|
|
88
|
+
* Build the validated tool with schema and handler.
|
|
57
89
|
*/
|
|
58
|
-
export
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
90
|
+
export function buildValidatedTestPageChangesTool(ctx) {
|
|
91
|
+
const tool = buildTestPageChangesTool(ctx);
|
|
92
|
+
return {
|
|
93
|
+
...tool,
|
|
94
|
+
inputSchema: TestPageChangesInputSchema,
|
|
95
|
+
handler: testPageChangesHandler,
|
|
96
|
+
};
|
|
97
|
+
}
|