@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
- // Track execution progress via state.stepsTaken from the API.
136
- // Setup is steps 1-3, execution maps stepsTaken into steps 4-28 (25 slots).
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 nodeCount = exec.nodeExecutions?.length ?? 0;
145
- const brainStepCount = (exec.nodeExecutions ?? [])
146
- .filter(n => n.nodeType === 'brain.step').length;
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: ${nodeCount}, steps: ${stepsTaken}`);
151
+ logger.info(`Execution status: ${exec.status}, nodes: ${nodes.length}, steps: ${stepsTaken}`);
153
152
  }
154
- if (progressCallback) {
155
- // If we see steps > our assumed max, bump our ceiling so progress never goes backwards
156
- if (stepsTaken > observedMaxSteps) {
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
- // Map stepsTaken (0..observedMaxSteps) into progress (SETUP_STEPS+1 .. TOTAL_STEPS-1)
160
- // Reserve the last tick for the "Complete" message
161
- let execProgress;
162
- if (stepsTaken > 0) {
163
- execProgress = SETUP_STEPS + Math.round((stepsTaken / observedMaxSteps) * (MAX_EXEC_STEPS - 1));
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
- // No steps yet show we're past setup but execution is starting
167
- execProgress = SETUP_STEPS + 1;
172
+ message = `Agent evaluating... (step ${stepsTaken})`;
168
173
  }
169
- execProgress = Math.min(execProgress, TOTAL_STEPS - 1);
170
- let message;
171
- if (exec.status === 'running') {
172
- if (stepsTaken > 0) {
173
- // Extract the latest brain.step to show what the agent is doing
174
- const latestStep = (exec.nodeExecutions ?? [])
175
- .filter(n => n.nodeType === 'brain.step' && n.outputData)
176
- .sort((a, b) => b.executionOrder - a.executionOrder)[0];
177
- const d = latestStep?.outputData?.decision ?? latestStep?.outputData;
178
- if (d) {
179
- const action = d.actionType ?? d.action_type ?? 'working';
180
- const intent = d.intent;
181
- message = intent
182
- ? `Step ${stepsTaken}: [${action}] ${intent}`
183
- : `Step ${stepsTaken}: ${action}`;
184
- }
185
- else {
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
- message = exec.status;
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 { tools, getTool } from "./tools/index.js";
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
  }
@@ -1,17 +1,25 @@
1
- import { testPageChangesTool, validatedTestPageChangesTool } from './testPageChanges.js';
2
- export const tools = [
3
- testPageChangesTool,
4
- ];
5
- export const validatedTools = [
6
- validatedTestPageChangesTool,
7
- ];
8
- export const toolRegistry = new Map();
9
- for (const tool of validatedTools) {
10
- toolRegistry.set(tool.name, tool);
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
- * Tool definition for testing page changes with DebuggAI
12
+ * Build the dynamic tool description including available environments/credentials.
9
13
  */
10
- export const testPageChangesTool = {
11
- name: "check_app_in_browser",
12
- title: "Run E2E Browser Test",
13
- 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.\n\nLOCALHOST 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. Supports localhost, 127.0.0.1, 0.0.0.0, [::1], and private IPs (192.168.x.x, 10.x.x.x). The tunnel stays alive for 55 minutes and is reused across calls to the same port.",
14
- inputSchema: {
15
- type: "object",
16
- properties: {
17
- description: {
18
- type: "string",
19
- 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')",
20
- minLength: 1
21
- },
22
- url: {
23
- type: "string",
24
- description: "URL to navigate to. Can be any public URL (https://example.com) OR a localhost/local dev server URL. For localhost URLs (http://localhost:3000, http://127.0.0.1:8080, etc.), a secure tunnel is automatically created so the remote browser can reach your machine — just make sure your dev server is running on that port. No extra setup needed."
25
- },
26
- environmentId: {
27
- type: "string",
28
- description: "UUID of a specific environment to use for this test"
29
- },
30
- credentialId: {
31
- type: "string",
32
- description: "UUID of a specific credential to use for login"
33
- },
34
- credentialRole: {
35
- type: "string",
36
- description: "Pick a credential by role (e.g. 'admin', 'guest') from the resolved environment"
37
- },
38
- username: {
39
- type: "string",
40
- description: "Username to log in with (creates or updates a credential idempotently)"
41
- },
42
- password: {
43
- type: "string",
44
- description: "Password to log in with (used together with username)"
45
- },
46
- repoName: {
47
- type: "string",
48
- 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."
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
- required: ["description", "url"],
52
- additionalProperties: false
53
- },
54
- };
85
+ };
86
+ }
55
87
  /**
56
- * Validated tool with schema and handler
88
+ * Build the validated tool with schema and handler.
57
89
  */
58
- export const validatedTestPageChangesTool = {
59
- ...testPageChangesTool,
60
- inputSchema: TestPageChangesInputSchema,
61
- handler: testPageChangesHandler,
62
- };
90
+ export function buildValidatedTestPageChangesTool(ctx) {
91
+ const tool = buildTestPageChangesTool(ctx);
92
+ return {
93
+ ...tool,
94
+ inputSchema: TestPageChangesInputSchema,
95
+ handler: testPageChangesHandler,
96
+ };
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debugg-ai/debugg-ai-mcp",
3
- "version": "1.0.45",
3
+ "version": "1.0.47",
4
4
  "description": "Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.",
5
5
  "type": "module",
6
6
  "bin": {