cbrowser 8.8.0 → 8.9.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 +9 -2
- package/dist/analysis/bug-hunter.js +1 -4
- package/dist/analysis/bug-hunter.js.map +1 -1
- package/dist/analysis/chaos-testing.js +4 -8
- package/dist/analysis/chaos-testing.js.map +1 -1
- package/dist/analysis/index.js +4 -20
- package/dist/analysis/index.js.map +1 -1
- package/dist/analysis/natural-language.js +4 -10
- package/dist/analysis/natural-language.js.map +1 -1
- package/dist/analysis/persona-comparison.js +6 -10
- package/dist/analysis/persona-comparison.js.map +1 -1
- package/dist/browser.d.ts +19 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +309 -132
- package/dist/browser.js.map +1 -1
- package/dist/cli.js +138 -131
- package/dist/cli.js.map +1 -1
- package/dist/cognitive/index.d.ts.map +1 -1
- package/dist/cognitive/index.js +61 -47
- package/dist/cognitive/index.js.map +1 -1
- package/dist/config.js +50 -61
- package/dist/config.js.map +1 -1
- package/dist/daemon.js +28 -37
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +10 -40
- package/dist/index.js.map +1 -1
- package/dist/mcp-server-remote.js +153 -156
- package/dist/mcp-server-remote.js.map +1 -1
- package/dist/mcp-server.js +146 -149
- package/dist/mcp-server.js.map +1 -1
- package/dist/performance/index.js +1 -17
- package/dist/performance/index.js.map +1 -1
- package/dist/performance/metrics.js +30 -39
- package/dist/performance/metrics.js.map +1 -1
- package/dist/personas.js +32 -46
- package/dist/personas.js.map +1 -1
- package/dist/testing/coverage.js +13 -23
- package/dist/testing/coverage.js.map +1 -1
- package/dist/testing/flaky-detection.js +4 -8
- package/dist/testing/flaky-detection.js.map +1 -1
- package/dist/testing/index.js +4 -20
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/nl-test-suite.js +11 -19
- package/dist/testing/nl-test-suite.js.map +1 -1
- package/dist/testing/test-repair.js +9 -15
- package/dist/testing/test-repair.js.map +1 -1
- package/dist/types.js +3 -6
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +9 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +60 -0
- package/dist/version.js.map +1 -0
- package/dist/visual/ab-comparison.js +16 -22
- package/dist/visual/ab-comparison.js.map +1 -1
- package/dist/visual/cross-browser.js +17 -24
- package/dist/visual/cross-browser.js.map +1 -1
- package/dist/visual/index.js +4 -20
- package/dist/visual/index.js.map +1 -1
- package/dist/visual/regression.js +46 -58
- package/dist/visual/regression.js.map +1 -1
- package/dist/visual/responsive.js +22 -29
- package/dist/visual/responsive.js.map +1 -1
- package/package.json +2 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
2
|
/**
|
|
4
3
|
* CBrowser Remote MCP Server
|
|
5
4
|
*
|
|
@@ -23,33 +22,31 @@
|
|
|
23
22
|
* AUTH0_AUDIENCE - API audience/identifier (e.g., 'https://cbrowser-mcp.wyldfyre.ai')
|
|
24
23
|
* AUTH0_CLIENT_ID - Optional: Client ID for static registration
|
|
25
24
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const browser_js_1 = require("./browser.js");
|
|
35
|
-
const config_js_1 = require("./config.js");
|
|
25
|
+
import { createServer } from "node:http";
|
|
26
|
+
import { randomUUID } from "node:crypto";
|
|
27
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
28
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
29
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
import { CBrowser } from "./browser.js";
|
|
32
|
+
import { ensureDirectories, getStatusInfo } from "./config.js";
|
|
36
33
|
// Visual module imports
|
|
37
|
-
|
|
34
|
+
import { runVisualRegression, runCrossBrowserTest, runResponsiveTest, runABComparison, crossBrowserDiff, captureVisualBaseline, listVisualBaselines, } from "./visual/index.js";
|
|
38
35
|
// Testing module imports
|
|
39
|
-
|
|
36
|
+
import { runNLTestSuite, parseNLTestSuite, dryRunNLTestSuite, repairTest, detectFlakyTests, generateCoverageMap, } from "./testing/index.js";
|
|
40
37
|
// Analysis module imports
|
|
41
|
-
|
|
38
|
+
import { huntBugs, runChaosTest, comparePersonas, findElementByIntent, } from "./analysis/index.js";
|
|
42
39
|
// Persona imports for cognitive journey
|
|
43
|
-
|
|
40
|
+
import { getPersona, listPersonas, getCognitiveProfile, createCognitivePersona, } from "./personas.js";
|
|
44
41
|
// Performance module imports
|
|
45
|
-
|
|
46
|
-
// Version
|
|
47
|
-
|
|
42
|
+
import { capturePerformanceBaseline, detectPerformanceRegression, listPerformanceBaselines, } from "./performance/index.js";
|
|
43
|
+
// Version from package.json - single source of truth
|
|
44
|
+
import { VERSION } from "./version.js";
|
|
48
45
|
// Shared browser instance
|
|
49
46
|
let browser = null;
|
|
50
47
|
async function getBrowser() {
|
|
51
48
|
if (!browser) {
|
|
52
|
-
browser = new
|
|
49
|
+
browser = new CBrowser({
|
|
53
50
|
headless: true,
|
|
54
51
|
persistent: true,
|
|
55
52
|
});
|
|
@@ -76,7 +73,7 @@ function getAuth0Config() {
|
|
|
76
73
|
domain,
|
|
77
74
|
audience,
|
|
78
75
|
clientId: process.env.AUTH0_CLIENT_ID,
|
|
79
|
-
jwks:
|
|
76
|
+
jwks: createRemoteJWKSet(new URL(`https://${domain}/.well-known/jwks.json`)),
|
|
80
77
|
};
|
|
81
78
|
}
|
|
82
79
|
return auth0Config;
|
|
@@ -99,7 +96,7 @@ async function validateAuth0Token(token) {
|
|
|
99
96
|
// If it's a proper JWT (3 parts), validate locally
|
|
100
97
|
if (tokenParts.length === 3) {
|
|
101
98
|
try {
|
|
102
|
-
const { payload } = await
|
|
99
|
+
const { payload } = await jwtVerify(token, config.jwks, {
|
|
103
100
|
issuer: `https://${config.domain}/`,
|
|
104
101
|
audience: config.audience,
|
|
105
102
|
});
|
|
@@ -255,7 +252,7 @@ function configureMcpTools(server) {
|
|
|
255
252
|
// Navigation Tools
|
|
256
253
|
// =========================================================================
|
|
257
254
|
server.tool("navigate", "Navigate to a URL and take a screenshot", {
|
|
258
|
-
url:
|
|
255
|
+
url: z.string().url().describe("The URL to navigate to"),
|
|
259
256
|
}, async ({ url }) => {
|
|
260
257
|
const b = await getBrowser();
|
|
261
258
|
const result = await b.navigate(url);
|
|
@@ -278,9 +275,9 @@ function configureMcpTools(server) {
|
|
|
278
275
|
// Interaction Tools
|
|
279
276
|
// =========================================================================
|
|
280
277
|
server.tool("click", "Click an element on the page using text, selector, or description. Use verbose=true for detailed debug info on failure.", {
|
|
281
|
-
selector:
|
|
282
|
-
force:
|
|
283
|
-
verbose:
|
|
278
|
+
selector: z.string().describe("Element to click (text content, CSS selector, or description)"),
|
|
279
|
+
force: z.boolean().optional().describe("Bypass safety checks for destructive actions"),
|
|
280
|
+
verbose: z.boolean().optional().describe("Return available elements and AI suggestions on failure"),
|
|
284
281
|
}, async ({ selector, force, verbose }) => {
|
|
285
282
|
const b = await getBrowser();
|
|
286
283
|
const result = await b.click(selector, { force, verbose });
|
|
@@ -302,9 +299,9 @@ function configureMcpTools(server) {
|
|
|
302
299
|
};
|
|
303
300
|
});
|
|
304
301
|
server.tool("smart_click", "Click with auto-retry and self-healing selectors", {
|
|
305
|
-
selector:
|
|
306
|
-
maxRetries:
|
|
307
|
-
dismissOverlays:
|
|
302
|
+
selector: z.string().describe("Element to click"),
|
|
303
|
+
maxRetries: z.number().optional().default(3).describe("Maximum retry attempts"),
|
|
304
|
+
dismissOverlays: z.boolean().optional().default(false).describe("Dismiss overlays before clicking"),
|
|
308
305
|
}, async ({ selector, maxRetries, dismissOverlays }) => {
|
|
309
306
|
const b = await getBrowser();
|
|
310
307
|
const result = await b.smartClick(selector, { maxRetries, dismissOverlays });
|
|
@@ -324,8 +321,8 @@ function configureMcpTools(server) {
|
|
|
324
321
|
};
|
|
325
322
|
});
|
|
326
323
|
server.tool("dismiss_overlay", "Detect and dismiss modal overlays (cookie consent, age verification, newsletter popups). Constitutional Yellow zone.", {
|
|
327
|
-
type:
|
|
328
|
-
customSelector:
|
|
324
|
+
type: z.enum(["auto", "cookie", "age-verify", "newsletter", "custom"]).optional().default("auto").describe("Overlay type to detect"),
|
|
325
|
+
customSelector: z.string().optional().describe("Custom CSS selector for overlay close button"),
|
|
329
326
|
}, async ({ type, customSelector }) => {
|
|
330
327
|
const b = await getBrowser();
|
|
331
328
|
const result = await b.dismissOverlay({ type, customSelector });
|
|
@@ -345,9 +342,9 @@ function configureMcpTools(server) {
|
|
|
345
342
|
};
|
|
346
343
|
});
|
|
347
344
|
server.tool("fill", "Fill a form field with text. Use verbose=true for detailed debug info on failure.", {
|
|
348
|
-
selector:
|
|
349
|
-
value:
|
|
350
|
-
verbose:
|
|
345
|
+
selector: z.string().describe("Input field to fill (name, placeholder, label, or selector)"),
|
|
346
|
+
value: z.string().describe("Value to enter"),
|
|
347
|
+
verbose: z.boolean().optional().describe("Return available inputs and AI suggestions on failure"),
|
|
351
348
|
}, async ({ selector, value, verbose }) => {
|
|
352
349
|
const b = await getBrowser();
|
|
353
350
|
const result = await b.fill(selector, value, { verbose });
|
|
@@ -371,7 +368,7 @@ function configureMcpTools(server) {
|
|
|
371
368
|
// Extraction Tools
|
|
372
369
|
// =========================================================================
|
|
373
370
|
server.tool("screenshot", "Take a screenshot of the current page", {
|
|
374
|
-
path:
|
|
371
|
+
path: z.string().optional().describe("Optional path to save the screenshot"),
|
|
375
372
|
}, async ({ path }) => {
|
|
376
373
|
const b = await getBrowser();
|
|
377
374
|
const file = await b.screenshot(path);
|
|
@@ -385,7 +382,7 @@ function configureMcpTools(server) {
|
|
|
385
382
|
};
|
|
386
383
|
});
|
|
387
384
|
server.tool("extract", "Extract data from the page", {
|
|
388
|
-
what:
|
|
385
|
+
what: z.enum(["links", "headings", "forms", "images", "text"]).describe("What to extract"),
|
|
389
386
|
}, async ({ what }) => {
|
|
390
387
|
const b = await getBrowser();
|
|
391
388
|
const result = await b.extract(what);
|
|
@@ -402,7 +399,7 @@ function configureMcpTools(server) {
|
|
|
402
399
|
// Assertion Tools
|
|
403
400
|
// =========================================================================
|
|
404
401
|
server.tool("assert", "Assert a condition using natural language", {
|
|
405
|
-
assertion:
|
|
402
|
+
assertion: z.string().describe("Natural language assertion like \"page contains 'Welcome'\" or \"title is 'Home'\""),
|
|
406
403
|
}, async ({ assertion }) => {
|
|
407
404
|
const b = await getBrowser();
|
|
408
405
|
const result = await b.assert(assertion);
|
|
@@ -444,7 +441,7 @@ function configureMcpTools(server) {
|
|
|
444
441
|
};
|
|
445
442
|
});
|
|
446
443
|
server.tool("generate_tests", "Generate test scenarios for a page", {
|
|
447
|
-
url:
|
|
444
|
+
url: z.string().url().optional().describe("URL to analyze (uses current page if not provided)"),
|
|
448
445
|
}, async ({ url }) => {
|
|
449
446
|
const b = await getBrowser();
|
|
450
447
|
const result = await b.generateTests(url);
|
|
@@ -468,7 +465,7 @@ function configureMcpTools(server) {
|
|
|
468
465
|
// Session Tools
|
|
469
466
|
// =========================================================================
|
|
470
467
|
server.tool("save_session", "Save browser session (cookies, storage) for later use", {
|
|
471
|
-
name:
|
|
468
|
+
name: z.string().describe("Name for the saved session"),
|
|
472
469
|
}, async ({ name }) => {
|
|
473
470
|
const b = await getBrowser();
|
|
474
471
|
await b.saveSession(name);
|
|
@@ -482,7 +479,7 @@ function configureMcpTools(server) {
|
|
|
482
479
|
};
|
|
483
480
|
});
|
|
484
481
|
server.tool("load_session", "Load a previously saved session", {
|
|
485
|
-
name:
|
|
482
|
+
name: z.string().describe("Name of the session to load"),
|
|
486
483
|
}, async ({ name }) => {
|
|
487
484
|
const b = await getBrowser();
|
|
488
485
|
const loaded = await b.loadSession(name);
|
|
@@ -508,7 +505,7 @@ function configureMcpTools(server) {
|
|
|
508
505
|
};
|
|
509
506
|
});
|
|
510
507
|
server.tool("delete_session", "Delete a saved session by name", {
|
|
511
|
-
name:
|
|
508
|
+
name: z.string().describe("Name of the session to delete"),
|
|
512
509
|
}, async ({ name }) => {
|
|
513
510
|
const b = await getBrowser();
|
|
514
511
|
const deleted = b.deleteSession(name);
|
|
@@ -540,10 +537,10 @@ function configureMcpTools(server) {
|
|
|
540
537
|
// Visual Testing Tools (v7.0.0+)
|
|
541
538
|
// =========================================================================
|
|
542
539
|
server.tool("visual_baseline", "Capture a visual baseline for a URL", {
|
|
543
|
-
url:
|
|
544
|
-
name:
|
|
540
|
+
url: z.string().url().describe("URL to capture baseline for"),
|
|
541
|
+
name: z.string().describe("Name for the baseline"),
|
|
545
542
|
}, async ({ url, name }) => {
|
|
546
|
-
const result = await
|
|
543
|
+
const result = await captureVisualBaseline(url, name, {});
|
|
547
544
|
return {
|
|
548
545
|
content: [
|
|
549
546
|
{
|
|
@@ -559,10 +556,10 @@ function configureMcpTools(server) {
|
|
|
559
556
|
};
|
|
560
557
|
});
|
|
561
558
|
server.tool("visual_regression", "Run AI visual regression test against a baseline", {
|
|
562
|
-
url:
|
|
563
|
-
baselineName:
|
|
559
|
+
url: z.string().url().describe("URL to test"),
|
|
560
|
+
baselineName: z.string().describe("Name of baseline to compare against"),
|
|
564
561
|
}, async ({ url, baselineName }) => {
|
|
565
|
-
const result = await
|
|
562
|
+
const result = await runVisualRegression(url, baselineName);
|
|
566
563
|
return {
|
|
567
564
|
content: [
|
|
568
565
|
{
|
|
@@ -578,10 +575,10 @@ function configureMcpTools(server) {
|
|
|
578
575
|
};
|
|
579
576
|
});
|
|
580
577
|
server.tool("cross_browser_test", "Test page rendering across multiple browsers", {
|
|
581
|
-
url:
|
|
582
|
-
browsers:
|
|
578
|
+
url: z.string().url().describe("URL to test"),
|
|
579
|
+
browsers: z.array(z.enum(["chromium", "firefox", "webkit"])).optional().describe("Browsers to test"),
|
|
583
580
|
}, async ({ url, browsers }) => {
|
|
584
|
-
const result = await
|
|
581
|
+
const result = await runCrossBrowserTest(url, { browsers });
|
|
585
582
|
return {
|
|
586
583
|
content: [
|
|
587
584
|
{
|
|
@@ -601,10 +598,10 @@ function configureMcpTools(server) {
|
|
|
601
598
|
};
|
|
602
599
|
});
|
|
603
600
|
server.tool("cross_browser_diff", "Quick diff of page metrics across browsers", {
|
|
604
|
-
url:
|
|
605
|
-
browsers:
|
|
601
|
+
url: z.string().url().describe("URL to compare"),
|
|
602
|
+
browsers: z.array(z.enum(["chromium", "firefox", "webkit"])).optional().describe("Browsers to compare"),
|
|
606
603
|
}, async ({ url, browsers }) => {
|
|
607
|
-
const result = await
|
|
604
|
+
const result = await crossBrowserDiff(url, browsers);
|
|
608
605
|
return {
|
|
609
606
|
content: [
|
|
610
607
|
{
|
|
@@ -623,10 +620,10 @@ function configureMcpTools(server) {
|
|
|
623
620
|
};
|
|
624
621
|
});
|
|
625
622
|
server.tool("responsive_test", "Test page across different viewport sizes", {
|
|
626
|
-
url:
|
|
627
|
-
viewports:
|
|
623
|
+
url: z.string().url().describe("URL to test"),
|
|
624
|
+
viewports: z.array(z.string()).optional().describe("Viewport presets (mobile, tablet, desktop, etc.)"),
|
|
628
625
|
}, async ({ url, viewports }) => {
|
|
629
|
-
const result = await
|
|
626
|
+
const result = await runResponsiveTest(url, { viewports });
|
|
630
627
|
return {
|
|
631
628
|
content: [
|
|
632
629
|
{
|
|
@@ -642,13 +639,13 @@ function configureMcpTools(server) {
|
|
|
642
639
|
};
|
|
643
640
|
});
|
|
644
641
|
server.tool("ab_comparison", "Compare two URLs visually (staging vs production)", {
|
|
645
|
-
urlA:
|
|
646
|
-
urlB:
|
|
647
|
-
labelA:
|
|
648
|
-
labelB:
|
|
642
|
+
urlA: z.string().url().describe("First URL (e.g., staging)"),
|
|
643
|
+
urlB: z.string().url().describe("Second URL (e.g., production)"),
|
|
644
|
+
labelA: z.string().optional().describe("Label for first URL"),
|
|
645
|
+
labelB: z.string().optional().describe("Label for second URL"),
|
|
649
646
|
}, async ({ urlA, urlB, labelA, labelB }) => {
|
|
650
647
|
const labels = labelA && labelB ? { a: labelA, b: labelB } : undefined;
|
|
651
|
-
const result = await
|
|
648
|
+
const result = await runABComparison(urlA, urlB, { labels });
|
|
652
649
|
return {
|
|
653
650
|
content: [
|
|
654
651
|
{
|
|
@@ -667,9 +664,9 @@ function configureMcpTools(server) {
|
|
|
667
664
|
// Testing Tools (v6.0.0+)
|
|
668
665
|
// =========================================================================
|
|
669
666
|
server.tool("nl_test_file", "Run natural language test suite from a file. Returns step-level results with enriched error info, partial matches, and suggestions.", {
|
|
670
|
-
filepath:
|
|
671
|
-
dryRun:
|
|
672
|
-
fuzzyMatch:
|
|
667
|
+
filepath: z.string().describe("Path to the test file"),
|
|
668
|
+
dryRun: z.boolean().optional().describe("Parse and display steps without executing"),
|
|
669
|
+
fuzzyMatch: z.boolean().optional().describe("Use case-insensitive fuzzy matching for assertions"),
|
|
673
670
|
}, async ({ filepath, dryRun, fuzzyMatch }) => {
|
|
674
671
|
const fs = await import("fs");
|
|
675
672
|
if (!fs.existsSync(filepath)) {
|
|
@@ -677,12 +674,12 @@ function configureMcpTools(server) {
|
|
|
677
674
|
}
|
|
678
675
|
const fileContent = fs.readFileSync(filepath, "utf-8");
|
|
679
676
|
const suiteName = filepath.split("/").pop()?.replace(/\.[^.]+$/, "") || "Test Suite";
|
|
680
|
-
const suite =
|
|
677
|
+
const suite = parseNLTestSuite(fileContent, suiteName);
|
|
681
678
|
if (dryRun) {
|
|
682
|
-
const dryResult =
|
|
679
|
+
const dryResult = dryRunNLTestSuite(suite);
|
|
683
680
|
return { content: [{ type: "text", text: JSON.stringify(dryResult, null, 2) }] };
|
|
684
681
|
}
|
|
685
|
-
const result = await
|
|
682
|
+
const result = await runNLTestSuite(suite, { fuzzyMatch: fuzzyMatch || false });
|
|
686
683
|
return {
|
|
687
684
|
content: [
|
|
688
685
|
{
|
|
@@ -715,17 +712,17 @@ function configureMcpTools(server) {
|
|
|
715
712
|
};
|
|
716
713
|
});
|
|
717
714
|
server.tool("nl_test_inline", "Run natural language tests from inline content. Returns step-level results with enriched error info, partial matches, and suggestions.", {
|
|
718
|
-
content:
|
|
719
|
-
name:
|
|
720
|
-
dryRun:
|
|
721
|
-
fuzzyMatch:
|
|
715
|
+
content: z.string().describe("Test content with instructions like 'go to https://...' and 'click login'"),
|
|
716
|
+
name: z.string().optional().describe("Name for the test suite"),
|
|
717
|
+
dryRun: z.boolean().optional().describe("Parse and display steps without executing"),
|
|
718
|
+
fuzzyMatch: z.boolean().optional().describe("Use case-insensitive fuzzy matching for assertions"),
|
|
722
719
|
}, async ({ content, name, dryRun, fuzzyMatch }) => {
|
|
723
|
-
const suite =
|
|
720
|
+
const suite = parseNLTestSuite(content, name || "Inline Test");
|
|
724
721
|
if (dryRun) {
|
|
725
|
-
const dryResult =
|
|
722
|
+
const dryResult = dryRunNLTestSuite(suite);
|
|
726
723
|
return { content: [{ type: "text", text: JSON.stringify(dryResult, null, 2) }] };
|
|
727
724
|
}
|
|
728
|
-
const result = await
|
|
725
|
+
const result = await runNLTestSuite(suite, { fuzzyMatch: fuzzyMatch || false });
|
|
729
726
|
return {
|
|
730
727
|
content: [
|
|
731
728
|
{
|
|
@@ -758,9 +755,9 @@ function configureMcpTools(server) {
|
|
|
758
755
|
};
|
|
759
756
|
});
|
|
760
757
|
server.tool("repair_test", "AI-powered test repair for broken tests", {
|
|
761
|
-
testName:
|
|
762
|
-
steps:
|
|
763
|
-
autoApply:
|
|
758
|
+
testName: z.string().describe("Name for the test"),
|
|
759
|
+
steps: z.array(z.string()).describe("Test step instructions"),
|
|
760
|
+
autoApply: z.boolean().optional().describe("Automatically apply repairs"),
|
|
764
761
|
}, async ({ testName, steps, autoApply }) => {
|
|
765
762
|
const testCase = {
|
|
766
763
|
name: testName,
|
|
@@ -769,7 +766,7 @@ function configureMcpTools(server) {
|
|
|
769
766
|
action: "unknown",
|
|
770
767
|
})),
|
|
771
768
|
};
|
|
772
|
-
const result = await
|
|
769
|
+
const result = await repairTest(testCase, { autoApply: autoApply || false });
|
|
773
770
|
return {
|
|
774
771
|
content: [
|
|
775
772
|
{
|
|
@@ -790,12 +787,12 @@ function configureMcpTools(server) {
|
|
|
790
787
|
};
|
|
791
788
|
});
|
|
792
789
|
server.tool("detect_flaky_tests", "Detect flaky/unreliable tests by running multiple times", {
|
|
793
|
-
testContent:
|
|
794
|
-
runs:
|
|
795
|
-
threshold:
|
|
790
|
+
testContent: z.string().describe("Test content to analyze"),
|
|
791
|
+
runs: z.number().optional().default(5).describe("Number of times to run each test"),
|
|
792
|
+
threshold: z.number().optional().default(20).describe("Flakiness threshold percentage"),
|
|
796
793
|
}, async ({ testContent, runs, threshold }) => {
|
|
797
|
-
const suite =
|
|
798
|
-
const result = await
|
|
794
|
+
const suite = parseNLTestSuite(testContent, "Flaky Test Analysis");
|
|
795
|
+
const result = await detectFlakyTests(suite, { runs, flakinessThreshold: threshold });
|
|
799
796
|
return {
|
|
800
797
|
content: [
|
|
801
798
|
{
|
|
@@ -819,11 +816,11 @@ function configureMcpTools(server) {
|
|
|
819
816
|
};
|
|
820
817
|
});
|
|
821
818
|
server.tool("coverage_map", "Generate test coverage map for a site", {
|
|
822
|
-
baseUrl:
|
|
823
|
-
testFiles:
|
|
824
|
-
maxPages:
|
|
819
|
+
baseUrl: z.string().url().describe("Base URL to analyze"),
|
|
820
|
+
testFiles: z.array(z.string()).describe("Array of test file paths"),
|
|
821
|
+
maxPages: z.number().optional().default(100).describe("Maximum pages to crawl"),
|
|
825
822
|
}, async ({ baseUrl, testFiles, maxPages }) => {
|
|
826
|
-
const result = await
|
|
823
|
+
const result = await generateCoverageMap(baseUrl, testFiles, { maxPages });
|
|
827
824
|
return {
|
|
828
825
|
content: [
|
|
829
826
|
{
|
|
@@ -847,12 +844,12 @@ function configureMcpTools(server) {
|
|
|
847
844
|
// Analysis Tools (v4.0.0+)
|
|
848
845
|
// =========================================================================
|
|
849
846
|
server.tool("hunt_bugs", "Autonomous bug hunting - crawl and find issues. Returns bugs with severity, selector, and actionable recommendation for each issue found.", {
|
|
850
|
-
url:
|
|
851
|
-
maxPages:
|
|
852
|
-
timeout:
|
|
847
|
+
url: z.string().url().describe("Starting URL to hunt from"),
|
|
848
|
+
maxPages: z.number().optional().default(10).describe("Maximum pages to visit"),
|
|
849
|
+
timeout: z.number().optional().default(60000).describe("Timeout in milliseconds"),
|
|
853
850
|
}, async ({ url, maxPages, timeout }) => {
|
|
854
851
|
const b = await getBrowser();
|
|
855
|
-
const result = await
|
|
852
|
+
const result = await huntBugs(b, url, { maxPages, timeout });
|
|
856
853
|
return {
|
|
857
854
|
content: [
|
|
858
855
|
{
|
|
@@ -875,13 +872,13 @@ function configureMcpTools(server) {
|
|
|
875
872
|
};
|
|
876
873
|
});
|
|
877
874
|
server.tool("chaos_test", "Inject failures and test resilience", {
|
|
878
|
-
url:
|
|
879
|
-
networkLatency:
|
|
880
|
-
offline:
|
|
881
|
-
blockUrls:
|
|
875
|
+
url: z.string().url().describe("URL to test"),
|
|
876
|
+
networkLatency: z.number().optional().describe("Simulate network latency (ms)"),
|
|
877
|
+
offline: z.boolean().optional().describe("Simulate offline mode"),
|
|
878
|
+
blockUrls: z.array(z.string()).optional().describe("URL patterns to block"),
|
|
882
879
|
}, async ({ url, networkLatency, offline, blockUrls }) => {
|
|
883
880
|
const b = await getBrowser();
|
|
884
|
-
const result = await
|
|
881
|
+
const result = await runChaosTest(b, url, { networkLatency, offline, blockUrls });
|
|
885
882
|
return {
|
|
886
883
|
content: [
|
|
887
884
|
{
|
|
@@ -896,11 +893,11 @@ function configureMcpTools(server) {
|
|
|
896
893
|
};
|
|
897
894
|
});
|
|
898
895
|
server.tool("compare_personas", "Compare how different user personas experience a journey", {
|
|
899
|
-
url:
|
|
900
|
-
goal:
|
|
901
|
-
personas:
|
|
896
|
+
url: z.string().url().describe("Starting URL"),
|
|
897
|
+
goal: z.string().describe("Goal to accomplish"),
|
|
898
|
+
personas: z.array(z.string()).describe("Persona names to compare"),
|
|
902
899
|
}, async ({ url, goal, personas }) => {
|
|
903
|
-
const result = await
|
|
900
|
+
const result = await comparePersonas({
|
|
904
901
|
startUrl: url,
|
|
905
902
|
goal,
|
|
906
903
|
personas,
|
|
@@ -920,11 +917,11 @@ function configureMcpTools(server) {
|
|
|
920
917
|
};
|
|
921
918
|
});
|
|
922
919
|
server.tool("find_element_by_intent", "AI-powered semantic element finding with ARIA-first selector strategy. Prioritizes aria-label > role > semantic HTML > ID > name > class. Returns selectorType, accessibilityScore (0-1), and alternatives. Use verbose=true for enriched failure responses.", {
|
|
923
|
-
intent:
|
|
924
|
-
verbose:
|
|
920
|
+
intent: z.string().describe("Natural language description like 'the cheapest product' or 'login form'"),
|
|
921
|
+
verbose: z.boolean().optional().describe("Include alternative matches with confidence scores and AI suggestions"),
|
|
925
922
|
}, async ({ intent, verbose }) => {
|
|
926
923
|
const b = await getBrowser();
|
|
927
|
-
const result = await
|
|
924
|
+
const result = await findElementByIntent(b, intent, { verbose });
|
|
928
925
|
if (result && result.confidence > 0) {
|
|
929
926
|
return {
|
|
930
927
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
@@ -938,25 +935,25 @@ function configureMcpTools(server) {
|
|
|
938
935
|
// Cognitive Simulation Tools (v8.3.0)
|
|
939
936
|
// =========================================================================
|
|
940
937
|
server.tool("cognitive_journey_init", "Initialize a cognitive user journey simulation. Returns the persona's cognitive profile, initial state, and abandonment thresholds. The actual simulation is driven by the LLM using browser tools (navigate, click, fill, screenshot) while tracking cognitive state.", {
|
|
941
|
-
persona:
|
|
942
|
-
goal:
|
|
943
|
-
startUrl:
|
|
944
|
-
customTraits:
|
|
945
|
-
patience:
|
|
946
|
-
riskTolerance:
|
|
947
|
-
comprehension:
|
|
948
|
-
persistence:
|
|
949
|
-
curiosity:
|
|
950
|
-
workingMemory:
|
|
951
|
-
readingTendency:
|
|
938
|
+
persona: z.string().describe("Persona name (e.g., 'first-timer', 'elderly-user', 'power-user') or custom description"),
|
|
939
|
+
goal: z.string().describe("What the simulated user is trying to accomplish"),
|
|
940
|
+
startUrl: z.string().url().describe("Starting URL for the journey"),
|
|
941
|
+
customTraits: z.object({
|
|
942
|
+
patience: z.number().min(0).max(1).optional(),
|
|
943
|
+
riskTolerance: z.number().min(0).max(1).optional(),
|
|
944
|
+
comprehension: z.number().min(0).max(1).optional(),
|
|
945
|
+
persistence: z.number().min(0).max(1).optional(),
|
|
946
|
+
curiosity: z.number().min(0).max(1).optional(),
|
|
947
|
+
workingMemory: z.number().min(0).max(1).optional(),
|
|
948
|
+
readingTendency: z.number().min(0).max(1).optional(),
|
|
952
949
|
}).optional().describe("Override specific cognitive traits"),
|
|
953
950
|
}, async ({ persona: personaName, goal, startUrl, customTraits }) => {
|
|
954
951
|
// Get or create persona
|
|
955
|
-
const existingPersona =
|
|
952
|
+
const existingPersona = getPersona(personaName);
|
|
956
953
|
let personaObj;
|
|
957
954
|
if (!existingPersona) {
|
|
958
955
|
// Create from description
|
|
959
|
-
personaObj =
|
|
956
|
+
personaObj = createCognitivePersona(personaName, personaName, customTraits || {});
|
|
960
957
|
}
|
|
961
958
|
else if (customTraits) {
|
|
962
959
|
// Merge custom traits with defaults
|
|
@@ -982,7 +979,7 @@ function configureMcpTools(server) {
|
|
|
982
979
|
personaObj = existingPersona;
|
|
983
980
|
}
|
|
984
981
|
// Get cognitive profile
|
|
985
|
-
const profile =
|
|
982
|
+
const profile = getCognitiveProfile(personaObj);
|
|
986
983
|
// Initial cognitive state
|
|
987
984
|
const initialState = {
|
|
988
985
|
patienceRemaining: 1.0,
|
|
@@ -1070,27 +1067,27 @@ Begin the simulation now. Narrate your thoughts as this persona.
|
|
|
1070
1067
|
};
|
|
1071
1068
|
});
|
|
1072
1069
|
server.tool("cognitive_journey_update_state", "Update the cognitive state during a journey simulation. Call this after each action to track mental state.", {
|
|
1073
|
-
currentState:
|
|
1074
|
-
patienceRemaining:
|
|
1075
|
-
confusionLevel:
|
|
1076
|
-
frustrationLevel:
|
|
1077
|
-
goalProgress:
|
|
1078
|
-
confidenceLevel:
|
|
1079
|
-
currentMood:
|
|
1080
|
-
stepCount:
|
|
1081
|
-
timeElapsed:
|
|
1070
|
+
currentState: z.object({
|
|
1071
|
+
patienceRemaining: z.number(),
|
|
1072
|
+
confusionLevel: z.number(),
|
|
1073
|
+
frustrationLevel: z.number(),
|
|
1074
|
+
goalProgress: z.number(),
|
|
1075
|
+
confidenceLevel: z.number(),
|
|
1076
|
+
currentMood: z.enum(["neutral", "hopeful", "confused", "frustrated", "defeated", "relieved"]),
|
|
1077
|
+
stepCount: z.number(),
|
|
1078
|
+
timeElapsed: z.number(),
|
|
1082
1079
|
}).describe("Current cognitive state"),
|
|
1083
|
-
actionResult:
|
|
1084
|
-
success:
|
|
1085
|
-
wasConfusing:
|
|
1086
|
-
progressMade:
|
|
1087
|
-
wentBack:
|
|
1080
|
+
actionResult: z.object({
|
|
1081
|
+
success: z.boolean(),
|
|
1082
|
+
wasConfusing: z.boolean().optional(),
|
|
1083
|
+
progressMade: z.boolean().optional(),
|
|
1084
|
+
wentBack: z.boolean().optional(),
|
|
1088
1085
|
}).describe("Result of the last action"),
|
|
1089
|
-
personaTraits:
|
|
1090
|
-
patience:
|
|
1091
|
-
riskTolerance:
|
|
1092
|
-
comprehension:
|
|
1093
|
-
persistence:
|
|
1086
|
+
personaTraits: z.object({
|
|
1087
|
+
patience: z.number(),
|
|
1088
|
+
riskTolerance: z.number(),
|
|
1089
|
+
comprehension: z.number(),
|
|
1090
|
+
persistence: z.number(),
|
|
1094
1091
|
}).describe("Persona traits affecting state changes"),
|
|
1095
1092
|
}, async ({ currentState, actionResult, personaTraits }) => {
|
|
1096
1093
|
// Calculate new state based on action result
|
|
@@ -1180,12 +1177,12 @@ Begin the simulation now. Narrate your thoughts as this persona.
|
|
|
1180
1177
|
};
|
|
1181
1178
|
});
|
|
1182
1179
|
server.tool("list_cognitive_personas", "List all available personas with their cognitive traits", {}, async () => {
|
|
1183
|
-
const names =
|
|
1180
|
+
const names = listPersonas();
|
|
1184
1181
|
const personas = names.map(name => {
|
|
1185
|
-
const p =
|
|
1182
|
+
const p = getPersona(name);
|
|
1186
1183
|
if (!p)
|
|
1187
1184
|
return null;
|
|
1188
|
-
const profile =
|
|
1185
|
+
const profile = getCognitiveProfile(p);
|
|
1189
1186
|
return {
|
|
1190
1187
|
name: p.name,
|
|
1191
1188
|
description: p.description,
|
|
@@ -1208,11 +1205,11 @@ Begin the simulation now. Narrate your thoughts as this persona.
|
|
|
1208
1205
|
// Performance Tools (v6.4.0+)
|
|
1209
1206
|
// =========================================================================
|
|
1210
1207
|
server.tool("perf_baseline", "Capture performance baseline for a URL", {
|
|
1211
|
-
url:
|
|
1212
|
-
name:
|
|
1213
|
-
runs:
|
|
1208
|
+
url: z.string().url().describe("URL to capture baseline for"),
|
|
1209
|
+
name: z.string().describe("Name for the baseline"),
|
|
1210
|
+
runs: z.number().optional().default(3).describe("Number of runs to average"),
|
|
1214
1211
|
}, async ({ url, name, runs }) => {
|
|
1215
|
-
const result = await
|
|
1212
|
+
const result = await capturePerformanceBaseline(url, { name, runs });
|
|
1216
1213
|
return {
|
|
1217
1214
|
content: [
|
|
1218
1215
|
{
|
|
@@ -1230,12 +1227,12 @@ Begin the simulation now. Narrate your thoughts as this persona.
|
|
|
1230
1227
|
};
|
|
1231
1228
|
});
|
|
1232
1229
|
server.tool("perf_regression", "Detect performance regression against baseline with configurable sensitivity. Uses dual thresholds: both percentage AND absolute change must be exceeded. Profiles: strict (CI/CD, FCP 10%/50ms), normal (default, FCP 20%/100ms), lenient (dev, FCP 30%/200ms). Sub-50ms FCP variations ignored by default.", {
|
|
1233
|
-
url:
|
|
1234
|
-
baselineName:
|
|
1235
|
-
sensitivity:
|
|
1236
|
-
thresholdLcp:
|
|
1230
|
+
url: z.string().url().describe("URL to test"),
|
|
1231
|
+
baselineName: z.string().describe("Name of baseline to compare against"),
|
|
1232
|
+
sensitivity: z.enum(["strict", "normal", "lenient"]).optional().default("normal").describe("Sensitivity profile: strict (CI/CD), normal (default), lenient (development)"),
|
|
1233
|
+
thresholdLcp: z.number().optional().describe("Override LCP threshold percentage"),
|
|
1237
1234
|
}, async ({ url, baselineName, sensitivity, thresholdLcp }) => {
|
|
1238
|
-
const result = await
|
|
1235
|
+
const result = await detectPerformanceRegression(url, baselineName, {
|
|
1239
1236
|
sensitivity,
|
|
1240
1237
|
thresholds: thresholdLcp ? { lcp: thresholdLcp } : undefined,
|
|
1241
1238
|
});
|
|
@@ -1256,8 +1253,8 @@ Begin the simulation now. Narrate your thoughts as this persona.
|
|
|
1256
1253
|
};
|
|
1257
1254
|
});
|
|
1258
1255
|
server.tool("list_baselines", "List all saved baselines (visual and performance)", {}, async () => {
|
|
1259
|
-
const visualBaselines = await
|
|
1260
|
-
const perfBaselines = await
|
|
1256
|
+
const visualBaselines = await listVisualBaselines();
|
|
1257
|
+
const perfBaselines = await listPerformanceBaselines();
|
|
1261
1258
|
return {
|
|
1262
1259
|
content: [
|
|
1263
1260
|
{
|
|
@@ -1272,7 +1269,7 @@ Begin the simulation now. Narrate your thoughts as this persona.
|
|
|
1272
1269
|
});
|
|
1273
1270
|
// Diagnostics
|
|
1274
1271
|
server.tool("status", "Get CBrowser environment status and diagnostics including data directories, installed browsers, configuration, and self-healing cache statistics", {}, async () => {
|
|
1275
|
-
const info = await
|
|
1272
|
+
const info = await getStatusInfo(VERSION);
|
|
1276
1273
|
return {
|
|
1277
1274
|
content: [
|
|
1278
1275
|
{
|
|
@@ -1287,7 +1284,7 @@ Begin the simulation now. Narrate your thoughts as this persona.
|
|
|
1287
1284
|
* Create a configured MCP server instance
|
|
1288
1285
|
*/
|
|
1289
1286
|
function createMcpServer() {
|
|
1290
|
-
const server = new
|
|
1287
|
+
const server = new McpServer({
|
|
1291
1288
|
name: "cbrowser",
|
|
1292
1289
|
version: VERSION,
|
|
1293
1290
|
});
|
|
@@ -1330,9 +1327,9 @@ async function handleMcpRequest(req, res, transport) {
|
|
|
1330
1327
|
/**
|
|
1331
1328
|
* Start the remote HTTP MCP server
|
|
1332
1329
|
*/
|
|
1333
|
-
async function startRemoteMcpServer() {
|
|
1330
|
+
export async function startRemoteMcpServer() {
|
|
1334
1331
|
// Auto-initialize all data directories on server start
|
|
1335
|
-
|
|
1332
|
+
ensureDirectories();
|
|
1336
1333
|
const port = parseInt(process.env.PORT || "3000", 10);
|
|
1337
1334
|
const host = process.env.HOST || "0.0.0.0";
|
|
1338
1335
|
const sessionMode = process.env.MCP_SESSION_MODE || "stateless";
|
|
@@ -1351,7 +1348,7 @@ async function startRemoteMcpServer() {
|
|
|
1351
1348
|
console.log(` - Auth0 OAuth: enabled (${auth0?.domain})`);
|
|
1352
1349
|
}
|
|
1353
1350
|
console.log(`Listening on ${host}:${port}`);
|
|
1354
|
-
const httpServer =
|
|
1351
|
+
const httpServer = createServer(async (req, res) => {
|
|
1355
1352
|
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
1356
1353
|
// CORS headers for all responses
|
|
1357
1354
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -1435,8 +1432,8 @@ async function startRemoteMcpServer() {
|
|
|
1435
1432
|
}
|
|
1436
1433
|
else {
|
|
1437
1434
|
// Create new transport
|
|
1438
|
-
transport = new
|
|
1439
|
-
sessionIdGenerator: sessionMode === "stateful" ? () =>
|
|
1435
|
+
transport = new StreamableHTTPServerTransport({
|
|
1436
|
+
sessionIdGenerator: sessionMode === "stateful" ? () => randomUUID() : undefined,
|
|
1440
1437
|
});
|
|
1441
1438
|
// Create and connect server
|
|
1442
1439
|
const server = createMcpServer();
|