@debugg-ai/debugg-ai-mcp 1.0.62 → 1.0.63

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.
@@ -1,4 +1,5 @@
1
1
  export * from './testPageChangesHandler.js';
2
+ export * from './triggerCrawlHandler.js';
2
3
  export * from './listEnvironmentsHandler.js';
3
4
  export * from './listCredentialsHandler.js';
4
5
  export * from './listProjectsHandler.js';
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Trigger Crawl Handler
3
+ *
4
+ * Executes the Raw Crawl Workflow Template via the 4-step pattern shared with
5
+ * testPageChangesHandler:
6
+ * find template → provision tunnel if localhost → execute → poll → result
7
+ *
8
+ * Unlike check_app_in_browser, a crawl does NOT return pass/fail — it returns
9
+ * the execution status + metadata. The backend's job is to explore the app
10
+ * and populate the project knowledge graph; this handler just triggers it
11
+ * and reports back what happened.
12
+ */
13
+ import { config } from '../config/index.js';
14
+ import { Logger } from '../utils/logger.js';
15
+ import { handleExternalServiceError } from '../utils/errors.js';
16
+ import { DebuggAIServerClient } from '../services/index.js';
17
+ import { resolveTargetUrl, buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
18
+ const logger = new Logger({ module: 'triggerCrawlHandler' });
19
+ const TEMPLATE_KEYWORD = 'raw crawl';
20
+ export async function triggerCrawlHandler(input, context, progressCallback) {
21
+ const startTime = Date.now();
22
+ logger.toolStart('trigger_crawl', input);
23
+ const client = new DebuggAIServerClient(config.api.key);
24
+ await client.init();
25
+ const originalUrl = resolveTargetUrl(input);
26
+ let ctx = buildContext(originalUrl);
27
+ let keyId;
28
+ const abortController = new AbortController();
29
+ const onStdinClose = () => abortController.abort();
30
+ process.stdin.once('close', onStdinClose);
31
+ try {
32
+ // --- Tunnel: reuse existing or provision a fresh one ---
33
+ if (ctx.isLocalhost) {
34
+ if (progressCallback) {
35
+ await progressCallback({ progress: 1, total: 4, message: 'Provisioning secure tunnel for localhost...' });
36
+ }
37
+ const reused = findExistingTunnel(ctx);
38
+ if (reused) {
39
+ ctx = reused;
40
+ }
41
+ else {
42
+ let tunnel;
43
+ try {
44
+ tunnel = await client.tunnels.provision();
45
+ }
46
+ catch (provisionError) {
47
+ const msg = provisionError instanceof Error ? provisionError.message : String(provisionError);
48
+ throw new Error(`Failed to provision tunnel for ${ctx.originalUrl}. ` +
49
+ `The remote browser needs a secure tunnel to reach your local dev server. ` +
50
+ `(Detail: ${msg})`);
51
+ }
52
+ keyId = tunnel.keyId;
53
+ ctx = await ensureTunnel(ctx, tunnel.tunnelKey, tunnel.tunnelId, tunnel.keyId, () => client.revokeNgrokKey(tunnel.keyId));
54
+ }
55
+ }
56
+ // --- Find the crawl workflow template ---
57
+ if (progressCallback) {
58
+ await progressCallback({ progress: 2, total: 4, message: 'Locating crawl workflow template...' });
59
+ }
60
+ const template = await client.workflows.findTemplateByName(TEMPLATE_KEYWORD);
61
+ if (!template) {
62
+ throw new Error(`Raw Crawl Workflow Template not found. ` +
63
+ `Ensure the backend has a template matching "${TEMPLATE_KEYWORD}" seeded and accessible.`);
64
+ }
65
+ const templateUuid = template.uuid;
66
+ logger.info(`Using crawl template: ${template.name} (${templateUuid})`);
67
+ // --- Build contextData + env ---
68
+ const contextData = {
69
+ targetUrl: ctx.targetUrl ?? ctx.originalUrl,
70
+ };
71
+ if (input.projectUuid)
72
+ contextData.projectId = input.projectUuid;
73
+ if (typeof input.headless === 'boolean')
74
+ contextData.headless = input.headless;
75
+ if (typeof input.timeoutSeconds === 'number')
76
+ contextData.timeoutSeconds = input.timeoutSeconds;
77
+ const env = {};
78
+ if (input.environmentId)
79
+ env.environmentId = input.environmentId;
80
+ if (input.credentialId)
81
+ env.credentialId = input.credentialId;
82
+ if (input.credentialRole)
83
+ env.credentialRole = input.credentialRole;
84
+ if (input.username)
85
+ env.username = input.username;
86
+ if (input.password)
87
+ env.password = input.password;
88
+ // --- Execute ---
89
+ if (progressCallback) {
90
+ await progressCallback({ progress: 3, total: 4, message: 'Queuing crawl execution...' });
91
+ }
92
+ const executeResponse = await client.workflows.executeWorkflow(templateUuid, contextData, Object.keys(env).length > 0 ? env : undefined);
93
+ const executionUuid = executeResponse.executionUuid;
94
+ logger.info(`Crawl execution queued: ${executionUuid}`);
95
+ // --- Poll ---
96
+ const finalExecution = await client.workflows.pollExecution(executionUuid, async (exec) => {
97
+ if (ctx.tunnelId)
98
+ touchTunnelById(ctx.tunnelId);
99
+ if (!progressCallback)
100
+ return;
101
+ const nodeCount = (exec.nodeExecutions ?? []).length;
102
+ await progressCallback({
103
+ progress: 4,
104
+ total: 4,
105
+ message: `Crawl ${exec.status} (${nodeCount} nodes)`,
106
+ });
107
+ }, abortController.signal);
108
+ const duration = Date.now() - startTime;
109
+ const nodes = finalExecution.nodeExecutions ?? [];
110
+ // --- Format response ---
111
+ const responsePayload = {
112
+ executionId: executionUuid,
113
+ status: finalExecution.status,
114
+ targetUrl: ctx.originalUrl,
115
+ durationMs: finalExecution.durationMs ?? duration,
116
+ };
117
+ const outcome = finalExecution.state?.outcome;
118
+ if (outcome !== undefined && outcome !== null)
119
+ responsePayload.outcome = outcome;
120
+ if (finalExecution.errorMessage)
121
+ responsePayload.errorMessage = finalExecution.errorMessage;
122
+ if (finalExecution.errorInfo?.failedNodeId)
123
+ responsePayload.failedNode = finalExecution.errorInfo.failedNodeId;
124
+ if (executeResponse.resolvedEnvironmentId)
125
+ responsePayload.resolvedEnvironmentId = executeResponse.resolvedEnvironmentId;
126
+ if (executeResponse.resolvedCredentialId)
127
+ responsePayload.resolvedCredentialId = executeResponse.resolvedCredentialId;
128
+ // Extract crawl metrics from surfer.crawl node (absent in older graph shapes)
129
+ const crawlNode = nodes.find(n => n.nodeType === 'surfer.crawl');
130
+ if (crawlNode?.outputData) {
131
+ const d = crawlNode.outputData;
132
+ responsePayload.crawlSummary = {
133
+ pagesDiscovered: d.pagesDiscovered,
134
+ actionsExecuted: d.actionsExecuted,
135
+ stepsTaken: d.stepsTaken,
136
+ transitionsRecorded: d.transitionsRecorded,
137
+ knowledgeGraphStates: d.knowledgeGraphStates,
138
+ success: d.success,
139
+ ...(d.error ? { error: d.error } : {}),
140
+ };
141
+ }
142
+ // Extract KG import result from knowledge_graph.import node (absent in older graph shapes)
143
+ const kgNode = nodes.find(n => n.nodeType === 'knowledge_graph.import');
144
+ if (kgNode?.outputData) {
145
+ const d = kgNode.outputData;
146
+ responsePayload.knowledgeGraph = {
147
+ imported: !d.skipped,
148
+ skipped: d.skipped ?? false,
149
+ reason: d.reason ?? '',
150
+ edgesImported: d.edgesImported ?? 0,
151
+ statesImported: d.statesImported ?? 0,
152
+ knowledgeGraphId: d.knowledgeGraphId ?? '',
153
+ ...(Array.isArray(d.importErrors) && d.importErrors.length > 0 ? { importErrors: d.importErrors } : {}),
154
+ };
155
+ }
156
+ logger.toolComplete('trigger_crawl', duration);
157
+ if (progressCallback) {
158
+ await progressCallback({ progress: 4, total: 4, message: `Crawl ${finalExecution.status}` });
159
+ }
160
+ const sanitizedPayload = sanitizeResponseUrls(responsePayload, ctx);
161
+ return {
162
+ content: [{ type: 'text', text: JSON.stringify(sanitizedPayload, null, 2) }],
163
+ };
164
+ }
165
+ catch (error) {
166
+ const duration = Date.now() - startTime;
167
+ logger.toolError('trigger_crawl', error, duration);
168
+ throw handleExternalServiceError(error, 'DebuggAI', 'crawl execution');
169
+ }
170
+ finally {
171
+ process.stdin.removeListener('close', onStdinClose);
172
+ // Tunnel intentionally NOT torn down (reuse path per bead vwd).
173
+ // If tunnel creation failed after key provision, revoke the orphaned key.
174
+ if (!ctx.tunnelId && keyId) {
175
+ client.revokeNgrokKey(keyId).catch(err => logger.warn(`Failed to revoke unused ngrok key ${keyId}: ${err}`));
176
+ }
177
+ }
178
+ }
@@ -8,18 +8,22 @@ const POLL_INTERVAL_MS = 3000;
8
8
  const EXECUTION_TIMEOUT_MS = 10 * 60 * 1000; // 10 min
9
9
  export const createWorkflowsService = (tx) => {
10
10
  const service = {
11
- async findEvaluationTemplate() {
11
+ async findTemplateByName(keyword) {
12
12
  const response = await tx.get('api/v1/workflows/', { isTemplate: true });
13
13
  const templates = response?.results ?? [];
14
14
  if (templates.length === 0)
15
15
  return null;
16
- const evalTemplate = templates.find(t => t.name.toLowerCase().includes('app evaluation'));
17
- if (!evalTemplate) {
18
- throw new Error(`No "App Evaluation" workflow template found. ` +
16
+ const needle = keyword.toLowerCase();
17
+ const match = templates.find(t => t.name.toLowerCase().includes(needle));
18
+ if (!match) {
19
+ throw new Error(`No workflow template matching "${keyword}" found. ` +
19
20
  `Available templates: ${templates.map(t => `"${t.name}"`).join(', ')}. ` +
20
- `Ensure the backend has a template with "App Evaluation" in its name.`);
21
+ `Ensure the backend has a template with "${keyword}" in its name.`);
21
22
  }
22
- return evalTemplate;
23
+ return match;
24
+ },
25
+ async findEvaluationTemplate() {
26
+ return service.findTemplateByName('app evaluation');
23
27
  },
24
28
  async executeWorkflow(workflowUuid, contextData, env) {
25
29
  const body = { contextData };
@@ -1,4 +1,5 @@
1
1
  import { buildTestPageChangesTool, buildValidatedTestPageChangesTool } from './testPageChanges.js';
2
+ import { buildTriggerCrawlTool, buildValidatedTriggerCrawlTool } from './triggerCrawl.js';
2
3
  import { buildListEnvironmentsTool, buildValidatedListEnvironmentsTool } from './listEnvironments.js';
3
4
  import { buildListCredentialsTool, buildValidatedListCredentialsTool } from './listCredentials.js';
4
5
  import { buildListProjectsTool, buildValidatedListProjectsTool } from './listProjects.js';
@@ -28,6 +29,7 @@ const toolRegistry = new Map();
28
29
  export function initTools(ctx) {
29
30
  const tools = [
30
31
  buildTestPageChangesTool(ctx),
32
+ buildTriggerCrawlTool(ctx),
31
33
  buildListProjectsTool(),
32
34
  buildListEnvironmentsTool(),
33
35
  buildListCredentialsTool(),
@@ -51,6 +53,7 @@ export function initTools(ctx) {
51
53
  ];
52
54
  const validated = [
53
55
  buildValidatedTestPageChangesTool(ctx),
56
+ buildValidatedTriggerCrawlTool(ctx),
54
57
  buildValidatedListProjectsTool(),
55
58
  buildValidatedListEnvironmentsTool(),
56
59
  buildValidatedListCredentialsTool(),
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Trigger Crawl Tool Definition
3
+ * Defines the trigger_crawl tool with proper validation.
4
+ * Tool description is enriched at startup with available environments/credentials.
5
+ */
6
+ import { TriggerCrawlInputSchema } from '../types/index.js';
7
+ import { triggerCrawlHandler } from '../handlers/triggerCrawlHandler.js';
8
+ const BASE_DESCRIPTION = `Trigger a browser-agent crawl of a web app to build the project's knowledge graph. The crawl systematically explores pages, UI states, and navigation flows, then populates the backend's knowledge graph so future evaluations and tests have context about the app.
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.
11
+
12
+ WHEN TO USE: after a significant new feature, a new environment, or when onboarding a project. NOT for per-change verification — use check_app_in_browser for that.
13
+
14
+ SCOPE: one crawl per call against one URL. The crawl is long-running (minutes to tens of minutes depending on app size) and populates backend state asynchronously; the tool returns the execution status once the workflow completes. This does NOT return pass/fail — it returns executionId + status + outcome.`;
15
+ export function buildTriggerCrawlDescription(ctx) {
16
+ if (!ctx)
17
+ return BASE_DESCRIPTION;
18
+ const envsWithCreds = ctx.environments.filter(e => e.credentials.length > 0);
19
+ if (envsWithCreds.length === 0) {
20
+ return `${BASE_DESCRIPTION}\n\nDETECTED PROJECT: "${ctx.project.name}" (repo: ${ctx.repoName}). No credentials configured — provide username/password if the app requires login to crawl authenticated areas.`;
21
+ }
22
+ const lines = [
23
+ `\n\nDETECTED PROJECT: "${ctx.project.name}" (repo: ${ctx.repoName})`,
24
+ `\nAVAILABLE ENVIRONMENTS & CREDENTIALS (pass environmentId + credentialId to crawl authenticated areas):`,
25
+ ];
26
+ for (const env of envsWithCreds) {
27
+ lines.push(`\n Environment: "${env.name}" (${env.uuid})${env.url ? ` — ${env.url}` : ''}`);
28
+ for (const cred of env.credentials) {
29
+ const parts = [` - "${cred.label}" (${cred.uuid}) — user: ${cred.username}`];
30
+ if (cred.role)
31
+ parts[0] += `, role: ${cred.role}`;
32
+ lines.push(parts[0]);
33
+ }
34
+ }
35
+ lines.push(`\nTo use: pass environmentId and credentialId from above. Or provide username/password directly.`);
36
+ return BASE_DESCRIPTION + lines.join('\n');
37
+ }
38
+ export function buildTriggerCrawlTool(ctx) {
39
+ return {
40
+ name: 'trigger_crawl',
41
+ title: 'Trigger App Crawl',
42
+ description: buildTriggerCrawlDescription(ctx),
43
+ inputSchema: {
44
+ type: 'object',
45
+ properties: {
46
+ url: {
47
+ type: 'string',
48
+ description: 'URL to crawl. Can be any public URL 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.',
49
+ },
50
+ projectUuid: {
51
+ type: 'string',
52
+ description: 'UUID of the project whose knowledge graph the crawl should populate. Auto-detected from the current git repo if omitted.',
53
+ },
54
+ environmentId: {
55
+ type: 'string',
56
+ description: 'UUID of a specific environment to use for the crawl. See available environments in the tool description above.',
57
+ },
58
+ credentialId: {
59
+ type: 'string',
60
+ description: 'UUID of a specific credential for authenticated crawls. See available credentials in the tool description above.',
61
+ },
62
+ credentialRole: {
63
+ type: 'string',
64
+ description: "Pick a credential by role (e.g. 'admin', 'guest') from the resolved environment.",
65
+ },
66
+ username: {
67
+ type: 'string',
68
+ description: 'A real, existing account email for the target app. Do NOT invent credentials — use one from the available credentials or ask the user.',
69
+ },
70
+ password: {
71
+ type: 'string',
72
+ description: 'The real password for the username above. Do NOT guess.',
73
+ },
74
+ headless: {
75
+ type: 'boolean',
76
+ description: 'Run the browser in headless mode. Defaults to backend configuration.',
77
+ },
78
+ timeoutSeconds: {
79
+ type: 'number',
80
+ description: 'Maximum wall-time the crawl may run, in seconds (1..1800). Backend enforces this per workflow execution.',
81
+ },
82
+ repoName: {
83
+ type: 'string',
84
+ description: "GitHub repository name (e.g. 'my-org/my-repo'). Auto-detected from the current git repo — only provide this to run against a different project.",
85
+ },
86
+ },
87
+ required: ['url'],
88
+ additionalProperties: false,
89
+ },
90
+ };
91
+ }
92
+ export function buildValidatedTriggerCrawlTool(ctx) {
93
+ const tool = buildTriggerCrawlTool(ctx);
94
+ return {
95
+ ...tool,
96
+ inputSchema: TriggerCrawlInputSchema,
97
+ handler: triggerCrawlHandler,
98
+ };
99
+ }
@@ -17,6 +17,18 @@ export const TestPageChangesInputSchema = z.object({
17
17
  password: z.string().optional(),
18
18
  repoName: z.string().optional(),
19
19
  });
20
+ export const TriggerCrawlInputSchema = z.object({
21
+ url: z.preprocess(normalizeUrl, z.string().url('Invalid URL. Pass a full URL like "http://localhost:3000" or "https://example.com". Localhost URLs are auto-tunneled to the remote browser.')),
22
+ projectUuid: z.string().uuid().optional(),
23
+ environmentId: z.string().uuid().optional(),
24
+ credentialId: z.string().uuid().optional(),
25
+ credentialRole: z.string().optional(),
26
+ username: z.string().optional(),
27
+ password: z.string().optional(),
28
+ headless: z.boolean().optional(),
29
+ timeoutSeconds: z.number().int().positive().max(1800, 'timeoutSeconds cannot exceed 1800 (30 min)').optional(),
30
+ repoName: z.string().optional(),
31
+ }).strict();
20
32
  export const ListEnvironmentsInputSchema = z.object({
21
33
  projectUuid: z.string().uuid().optional(),
22
34
  q: z.string().min(1).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debugg-ai/debugg-ai-mcp",
3
- "version": "1.0.62",
3
+ "version": "1.0.63",
4
4
  "description": "Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.",
5
5
  "type": "module",
6
6
  "bin": {