cbrowser 19.6.0 → 20.0.0
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 +292 -232
- package/THIRD_PARTY_LICENSES.md +416 -7
- package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
- package/dist/analysis/accessibility-empathy.js +2 -0
- package/dist/analysis/accessibility-empathy.js.map +1 -1
- package/dist/analysis/competitive-benchmark.d.ts.map +1 -1
- package/dist/analysis/competitive-benchmark.js +70 -558
- package/dist/analysis/competitive-benchmark.js.map +1 -1
- package/dist/analysis/natural-language.d.ts.map +1 -1
- package/dist/analysis/natural-language.js +43 -0
- package/dist/analysis/natural-language.js.map +1 -1
- package/dist/analysis/persona-comparison.d.ts +7 -0
- package/dist/analysis/persona-comparison.d.ts.map +1 -1
- package/dist/analysis/persona-comparison.js +8 -1
- package/dist/analysis/persona-comparison.js.map +1 -1
- package/dist/browser-options.d.ts +23 -0
- package/dist/browser-options.d.ts.map +1 -0
- package/dist/browser-options.js +24 -0
- package/dist/browser-options.js.map +1 -0
- package/dist/browser.d.ts +36 -14
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +139 -196
- package/dist/browser.js.map +1 -1
- package/dist/cli-args.d.ts +9 -1
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +35 -7
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-browser-flags.d.ts +33 -0
- package/dist/cli-browser-flags.d.ts.map +1 -0
- package/dist/cli-browser-flags.js +89 -0
- package/dist/cli-browser-flags.js.map +1 -0
- package/dist/cli.js +979 -401
- package/dist/cli.js.map +1 -1
- package/dist/cognitive/index.d.ts +0 -13
- package/dist/cognitive/index.d.ts.map +1 -1
- package/dist/cognitive/index.js +39 -15
- package/dist/cognitive/index.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -6
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +5 -4
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +87 -29
- package/dist/daemon.js.map +1 -1
- package/dist/devices.d.ts +20 -0
- package/dist/devices.d.ts.map +1 -1
- package/dist/devices.js +18 -0
- package/dist/devices.js.map +1 -1
- package/dist/help-client.d.ts +48 -0
- package/dist/help-client.d.ts.map +1 -0
- package/dist/help-client.js +96 -0
- package/dist/help-client.js.map +1 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +165 -2281
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-tools/base/advanced-interaction-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/advanced-interaction-tools.js +21 -21
- package/dist/mcp-tools/base/advanced-interaction-tools.js.map +1 -1
- package/dist/mcp-tools/base/assertion-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/assertion-tools.js +6 -3
- package/dist/mcp-tools/base/assertion-tools.js.map +1 -1
- package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/audit-tools.js +15 -4
- package/dist/mcp-tools/base/audit-tools.js.map +1 -1
- package/dist/mcp-tools/base/browser-state-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/browser-state-tools.js +33 -27
- package/dist/mcp-tools/base/browser-state-tools.js.map +1 -1
- package/dist/mcp-tools/base/bug-analysis-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/bug-analysis-tools.js +9 -2
- package/dist/mcp-tools/base/bug-analysis-tools.js.map +1 -1
- package/dist/mcp-tools/base/capture-tools.d.ts +60 -4
- package/dist/mcp-tools/base/capture-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/capture-tools.js +118 -14
- package/dist/mcp-tools/base/capture-tools.js.map +1 -1
- package/dist/mcp-tools/base/index.d.ts +6 -5
- package/dist/mcp-tools/base/index.d.ts.map +1 -1
- package/dist/mcp-tools/base/index.js +7 -6
- package/dist/mcp-tools/base/index.js.map +1 -1
- package/dist/mcp-tools/base/interaction-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/interaction-tools.js +20 -6
- package/dist/mcp-tools/base/interaction-tools.js.map +1 -1
- package/dist/mcp-tools/base/navigation-tools.d.ts +1 -1
- package/dist/mcp-tools/base/navigation-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/navigation-tools.js +83 -3
- package/dist/mcp-tools/base/navigation-tools.js.map +1 -1
- package/dist/mcp-tools/base/site-knowledge-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/site-knowledge-tools.js +11 -23
- package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.js +13 -3
- package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
- package/dist/mcp-tools/enterprise-stubs.d.ts +2 -2
- package/dist/mcp-tools/enterprise-stubs.d.ts.map +1 -1
- package/dist/mcp-tools/enterprise-stubs.js +29 -16
- package/dist/mcp-tools/enterprise-stubs.js.map +1 -1
- package/dist/mcp-tools/index.d.ts +19 -14
- package/dist/mcp-tools/index.d.ts.map +1 -1
- package/dist/mcp-tools/index.js +2 -2
- package/dist/mcp-tools/index.js.map +1 -1
- package/dist/mcp-tools/red-zone-refusal.d.ts +33 -0
- package/dist/mcp-tools/red-zone-refusal.d.ts.map +1 -0
- package/dist/mcp-tools/red-zone-refusal.js +43 -0
- package/dist/mcp-tools/red-zone-refusal.js.map +1 -0
- package/dist/mcp-tools/tier-gate.js +1 -1
- package/dist/mcp-tools/tier-gate.js.map +1 -1
- package/dist/mcp-tools/tool-categories.d.ts.map +1 -1
- package/dist/mcp-tools/tool-categories.js +2 -0
- package/dist/mcp-tools/tool-categories.js.map +1 -1
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +18 -2
- package/dist/output.js.map +1 -1
- package/dist/performance/metrics.d.ts +8 -3
- package/dist/performance/metrics.d.ts.map +1 -1
- package/dist/performance/metrics.js +16 -5
- package/dist/performance/metrics.js.map +1 -1
- package/dist/recording/contact-sheet.js.map +1 -1
- package/dist/security/tool-permissions.d.ts.map +1 -1
- package/dist/security/tool-permissions.js +1 -0
- package/dist/security/tool-permissions.js.map +1 -1
- package/dist/testing/coverage.d.ts +2 -2
- package/dist/testing/coverage.d.ts.map +1 -1
- package/dist/testing/coverage.js +6 -2
- package/dist/testing/coverage.js.map +1 -1
- package/dist/testing/flaky-detection.d.ts +2 -2
- package/dist/testing/flaky-detection.d.ts.map +1 -1
- package/dist/testing/flaky-detection.js +11 -3
- package/dist/testing/flaky-detection.js.map +1 -1
- package/dist/testing/nl-test-suite.d.ts +13 -0
- package/dist/testing/nl-test-suite.d.ts.map +1 -1
- package/dist/testing/nl-test-suite.js +5 -1
- package/dist/testing/nl-test-suite.js.map +1 -1
- package/dist/testing/suite-results.d.ts +27 -0
- package/dist/testing/suite-results.d.ts.map +1 -0
- package/dist/testing/suite-results.js +54 -0
- package/dist/testing/suite-results.js.map +1 -0
- package/dist/testing/test-repair.d.ts +2 -2
- package/dist/testing/test-repair.d.ts.map +1 -1
- package/dist/testing/test-repair.js +6 -2
- package/dist/testing/test-repair.js.map +1 -1
- package/dist/types.d.ts +61 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -4
- package/dist/types.js.map +1 -1
- package/dist/visual/ab-comparison.d.ts.map +1 -1
- package/dist/visual/ab-comparison.js +7 -2
- package/dist/visual/ab-comparison.js.map +1 -1
- package/dist/visual/cross-browser.d.ts +7 -2
- package/dist/visual/cross-browser.d.ts.map +1 -1
- package/dist/visual/cross-browser.js +33 -5
- package/dist/visual/cross-browser.js.map +1 -1
- package/dist/visual/distance-metrics.d.ts +7 -0
- package/dist/visual/distance-metrics.d.ts.map +1 -1
- package/dist/visual/distance-metrics.js.map +1 -1
- package/dist/visual/heatmap-overlay.js.map +1 -1
- package/dist/visual/llm-relevance.d.ts.map +1 -1
- package/dist/visual/llm-relevance.js.map +1 -1
- package/dist/visual/regression.d.ts +17 -7
- package/dist/visual/regression.d.ts.map +1 -1
- package/dist/visual/regression.js +77 -38
- package/dist/visual/regression.js.map +1 -1
- package/dist/visual/responsive.d.ts +22 -0
- package/dist/visual/responsive.d.ts.map +1 -1
- package/dist/visual/responsive.js +44 -7
- package/dist/visual/responsive.js.map +1 -1
- package/dist/visual/visual-overlays.js.map +1 -1
- package/dist/webhooks.d.ts +67 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +128 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +23 -24
package/dist/mcp-server.js
CHANGED
|
@@ -15,36 +15,18 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
15
15
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
import { CBrowser } from "./browser.js";
|
|
18
|
-
import { ensureDirectories
|
|
19
|
-
|
|
20
|
-
import { startCapture, stopCapture, captureStatus, CAPTURE_START_DESCRIPTION, CAPTURE_STOP_DESCRIPTION, CAPTURE_STATUS_DESCRIPTION, } from "./mcp-tools/base/capture-tools.js";
|
|
21
|
-
import { registerEmpathyAuditTool } from "./mcp-tools/base/audit-tools.js";
|
|
22
|
-
import { registerValuesTools } from "./mcp-tools/base/values-tools.js";
|
|
23
|
-
import { registerPersonaComparisonTools } from "./mcp-tools/base/persona-comparison-tools.js";
|
|
24
|
-
import { registerPersonaCreationTools } from "./mcp-tools/persona-creation-tools.js";
|
|
25
|
-
import { registerPersonaLifecycleTools } from "./mcp-tools/persona-lifecycle-tools.js";
|
|
18
|
+
import { ensureDirectories } from "./config.js";
|
|
19
|
+
import { registerAllPublicTools } from "./mcp-tools/index.js";
|
|
26
20
|
import { registerUiResources } from "./mcp-tools/ui-resources.js";
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
import { runNLTestSuite, parseNLTestSuite, dryRunNLTestSuite, repairTest, detectFlakyTests, generateCoverageMap, } from "./testing/index.js";
|
|
33
|
-
// Analysis module imports
|
|
34
|
-
import { huntBugs, huntBugsResponse, runChaosTest, findElementByIntent, FIND_ELEMENT_BY_INTENT_DESCRIPTION, runAgentReadyAudit, pageAgentReadyFindings, runCompetitiveBenchmark, countAffectedElements, } from "./analysis/index.js";
|
|
35
|
-
import { listAccessibilityPersonas, getAccessibilityPersona, describeAccessibilityPersona } from "./personas.js";
|
|
36
|
-
// Persona imports for cognitive journey
|
|
37
|
-
import { getPersona, getAnyPersona, listPersonas, getCognitiveProfile, createCognitivePersona, listEmotionalPersonas, getEmotionalPersona, isAgentPersonaObject, } from "./personas.js";
|
|
21
|
+
import { setSecurityAuditToolList } from "./mcp-tools/base/security-tools.js";
|
|
22
|
+
import { countAffectedElements } from "./analysis/index.js";
|
|
23
|
+
import { listAccessibilityPersonas, getAccessibilityPersona } from "./personas.js";
|
|
24
|
+
// Persona imports for the emotional-state tools
|
|
25
|
+
import { getPersona, listEmotionalPersonas, getEmotionalPersona, } from "./personas.js";
|
|
38
26
|
// Emotional state functions (v13.1.0)
|
|
39
27
|
import { createInitialEmotionalState, createEmotionalConfig, applyEmotionalTrigger, describeEmotionalState, shouldConsiderAbandonment, calculateAbandonmentModifier, calculateExplorationTendency, calculateDecisionSpeedModifier, } from "./cognitive/emotions.js";
|
|
40
|
-
// Performance module imports
|
|
41
|
-
import { capturePerformanceBaseline, detectPerformanceRegression, listPerformanceBaselines, } from "./performance/index.js";
|
|
42
|
-
// Values system (Schwartz's 10 Universal Values)
|
|
43
|
-
import { resolveValuesForPersona, rankInfluencePatternsForProfile, } from "./values/index.js";
|
|
44
28
|
// Version from package.json - single source of truth
|
|
45
29
|
import { VERSION } from "./version.js";
|
|
46
|
-
// Security tools (mcp-guardian)
|
|
47
|
-
import { securityAuditHandler, } from "mcp-guardian";
|
|
48
30
|
// Shared browser instance
|
|
49
31
|
let browser = null;
|
|
50
32
|
async function getBrowser() {
|
|
@@ -56,247 +38,6 @@ async function getBrowser() {
|
|
|
56
38
|
}
|
|
57
39
|
return browser;
|
|
58
40
|
}
|
|
59
|
-
/**
|
|
60
|
-
* v14.2.1: Retry wrapper for transient browser errors.
|
|
61
|
-
* v14.2.5: Fixed page context desync after error recovery.
|
|
62
|
-
* Retries operations that fail with common transient error patterns.
|
|
63
|
-
*
|
|
64
|
-
* v16.11.0: CRASH RESILIENCE PATTERN
|
|
65
|
-
* For tools that use context-level operations (setOffline, route interception),
|
|
66
|
-
* use an explicit try-catch + recovery pattern instead of withRetry:
|
|
67
|
-
*
|
|
68
|
-
* try {
|
|
69
|
-
* const result = await dangerousOperation();
|
|
70
|
-
* return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
71
|
-
* } catch (error: any) {
|
|
72
|
-
* try { await browser.recoverBrowser(); } catch { }
|
|
73
|
-
* return { content: [{ type: "text", text: JSON.stringify({
|
|
74
|
-
* error: error.message, recovered: true
|
|
75
|
-
* }) }] };
|
|
76
|
-
* }
|
|
77
|
-
*
|
|
78
|
-
* This ensures the MCP server never crashes from unhandled browser errors.
|
|
79
|
-
* See chaos_test for the reference implementation.
|
|
80
|
-
*/
|
|
81
|
-
async function withRetry(operation, options = {}) {
|
|
82
|
-
const maxRetries = options.maxRetries ?? 2;
|
|
83
|
-
const retryDelay = options.retryDelay ?? 500;
|
|
84
|
-
let lastError = null;
|
|
85
|
-
// v14.2.5: Capture current URL before operation to restore after recovery
|
|
86
|
-
let expectedUrl = null;
|
|
87
|
-
try {
|
|
88
|
-
const b = await getBrowser();
|
|
89
|
-
const page = await b.getPage();
|
|
90
|
-
expectedUrl = page.url();
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
// Can't get URL, proceed without context preservation
|
|
94
|
-
}
|
|
95
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
96
|
-
try {
|
|
97
|
-
return await operation();
|
|
98
|
-
}
|
|
99
|
-
catch (e) {
|
|
100
|
-
lastError = e;
|
|
101
|
-
const errorMessage = lastError.message || "";
|
|
102
|
-
// Check if this is a transient error worth retrying
|
|
103
|
-
const isTransient = errorMessage.includes("Target closed") ||
|
|
104
|
-
errorMessage.includes("Execution context") ||
|
|
105
|
-
errorMessage.includes("Session closed") ||
|
|
106
|
-
errorMessage.includes("Connection refused") ||
|
|
107
|
-
errorMessage.includes("Browser disconnected");
|
|
108
|
-
if (!isTransient || attempt === maxRetries) {
|
|
109
|
-
throw lastError;
|
|
110
|
-
}
|
|
111
|
-
// Wait before retry with exponential backoff
|
|
112
|
-
await new Promise((r) => setTimeout(r, retryDelay * (attempt + 1)));
|
|
113
|
-
// Try to recover the browser before retrying
|
|
114
|
-
try {
|
|
115
|
-
const b = await getBrowser();
|
|
116
|
-
await b.recoverBrowser();
|
|
117
|
-
// v14.2.5: Verify page context matches expected URL after recovery
|
|
118
|
-
// This prevents desync where recovery loads a different saved session
|
|
119
|
-
if (expectedUrl && expectedUrl !== "about:blank") {
|
|
120
|
-
const page = await b.getPage();
|
|
121
|
-
const currentUrl = page.url();
|
|
122
|
-
if (currentUrl !== expectedUrl && !currentUrl.startsWith(expectedUrl.split("?")[0])) {
|
|
123
|
-
// Page recovered to wrong URL, navigate back
|
|
124
|
-
await page.goto(expectedUrl, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
// Recovery failed, will retry operation anyway
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
throw lastError;
|
|
134
|
-
}
|
|
135
|
-
const VALUES_QUESTIONS = [
|
|
136
|
-
{
|
|
137
|
-
id: "security", value: "security",
|
|
138
|
-
question: "How important is safety and stability to this persona?",
|
|
139
|
-
options: [
|
|
140
|
-
{ value: 0.0, label: "Not Important", description: "Takes risks freely, ignores safety warnings" },
|
|
141
|
-
{ value: 0.33, label: "Somewhat Important", description: "Considers safety but willing to take chances" },
|
|
142
|
-
{ value: 0.67, label: "Important", description: "Prefers established, secure options" },
|
|
143
|
-
{ value: 1.0, label: "Very Important", description: "Prioritizes safety above almost everything" },
|
|
144
|
-
],
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
id: "stimulation", value: "stimulation",
|
|
148
|
-
question: "How much does this persona seek excitement and novelty?",
|
|
149
|
-
options: [
|
|
150
|
-
{ value: 0.0, label: "Avoids", description: "Prefers predictable, calm experiences" },
|
|
151
|
-
{ value: 0.33, label: "Occasionally", description: "Open to new things but not seeking them" },
|
|
152
|
-
{ value: 0.67, label: "Seeks", description: "Actively looks for new and exciting experiences" },
|
|
153
|
-
{ value: 1.0, label: "Craves", description: "Constantly seeking stimulation and novelty" },
|
|
154
|
-
],
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
id: "achievement", value: "achievement",
|
|
158
|
-
question: "How driven is this persona by personal success and competence?",
|
|
159
|
-
options: [
|
|
160
|
-
{ value: 0.0, label: "Not Driven", description: "Success is not a priority" },
|
|
161
|
-
{ value: 0.33, label: "Moderately", description: "Likes to succeed but not at all costs" },
|
|
162
|
-
{ value: 0.67, label: "Driven", description: "Works hard to demonstrate competence" },
|
|
163
|
-
{ value: 1.0, label: "Highly Driven", description: "Success and achievement are paramount" },
|
|
164
|
-
],
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
id: "conformity", value: "conformity",
|
|
168
|
-
question: "How much does this persona follow social expectations and norms?",
|
|
169
|
-
options: [
|
|
170
|
-
{ value: 0.0, label: "Independent", description: "Makes own rules, ignores conventions" },
|
|
171
|
-
{ value: 0.33, label: "Flexible", description: "Follows norms when convenient" },
|
|
172
|
-
{ value: 0.67, label: "Compliant", description: "Generally follows social expectations" },
|
|
173
|
-
{ value: 1.0, label: "Traditional", description: "Strongly adheres to social norms" },
|
|
174
|
-
],
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
id: "hedonism", value: "hedonism",
|
|
178
|
-
question: "How much does this persona prioritize pleasure and enjoyment?",
|
|
179
|
-
options: [
|
|
180
|
-
{ value: 0.0, label: "Practical", description: "Prioritizes function over pleasure" },
|
|
181
|
-
{ value: 0.33, label: "Balanced", description: "Enjoys pleasure but not a priority" },
|
|
182
|
-
{ value: 0.67, label: "Pleasure-Seeking", description: "Actively seeks enjoyable experiences" },
|
|
183
|
-
{ value: 1.0, label: "Hedonistic", description: "Pleasure and enjoyment are top priorities" },
|
|
184
|
-
],
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
id: "power", value: "power",
|
|
188
|
-
question: "How important is social status and influence to this persona?",
|
|
189
|
-
options: [
|
|
190
|
-
{ value: 0.0, label: "Not Important", description: "Doesn't care about status or control" },
|
|
191
|
-
{ value: 0.33, label: "Minor Concern", description: "Aware of status but not driven by it" },
|
|
192
|
-
{ value: 0.67, label: "Important", description: "Values influence and recognition" },
|
|
193
|
-
{ value: 1.0, label: "Critical", description: "Status and control are major motivators" },
|
|
194
|
-
],
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
id: "tradition", value: "tradition",
|
|
198
|
-
question: "How much does this persona value cultural and family traditions?",
|
|
199
|
-
options: [
|
|
200
|
-
{ value: 0.0, label: "Progressive", description: "Embraces change, questions traditions" },
|
|
201
|
-
{ value: 0.33, label: "Moderate", description: "Respects traditions but open to change" },
|
|
202
|
-
{ value: 0.67, label: "Traditional", description: "Values and maintains traditions" },
|
|
203
|
-
{ value: 1.0, label: "Strongly Traditional", description: "Traditions are core to identity" },
|
|
204
|
-
],
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
id: "benevolence", value: "benevolence",
|
|
208
|
-
question: "How much does this persona prioritize caring for close others?",
|
|
209
|
-
options: [
|
|
210
|
-
{ value: 0.0, label: "Self-Focused", description: "Prioritizes own needs over others" },
|
|
211
|
-
{ value: 0.33, label: "Balanced", description: "Cares for others when convenient" },
|
|
212
|
-
{ value: 0.67, label: "Caring", description: "Actively helps and supports close others" },
|
|
213
|
-
{ value: 1.0, label: "Devoted", description: "Others' welfare is a top priority" },
|
|
214
|
-
],
|
|
215
|
-
},
|
|
216
|
-
{
|
|
217
|
-
id: "universalism", value: "universalism",
|
|
218
|
-
question: "How much does this persona care about broader social and environmental issues?",
|
|
219
|
-
options: [
|
|
220
|
-
{ value: 0.0, label: "Narrow Focus", description: "Focuses on immediate concerns only" },
|
|
221
|
-
{ value: 0.33, label: "Aware", description: "Somewhat concerned about broader issues" },
|
|
222
|
-
{ value: 0.67, label: "Engaged", description: "Actively considers social/environmental impact" },
|
|
223
|
-
{ value: 1.0, label: "Activist", description: "Deeply committed to social justice and environment" },
|
|
224
|
-
],
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
id: "selfDirection", value: "selfDirection",
|
|
228
|
-
question: "How much does this persona value independence and autonomy?",
|
|
229
|
-
options: [
|
|
230
|
-
{ value: 0.0, label: "Guided", description: "Prefers clear direction from others" },
|
|
231
|
-
{ value: 0.33, label: "Moderate", description: "Likes some guidance but can be independent" },
|
|
232
|
-
{ value: 0.67, label: "Independent", description: "Prefers making own choices" },
|
|
233
|
-
{ value: 1.0, label: "Autonomous", description: "Strongly values independence and self-reliance" },
|
|
234
|
-
],
|
|
235
|
-
},
|
|
236
|
-
];
|
|
237
|
-
const questionnaireSessionsMap = new Map();
|
|
238
|
-
/**
|
|
239
|
-
* Group a flat Schwartz/SDT value record into the nested schema every other
|
|
240
|
-
* values-returning path uses (schwartz / higherOrder / sdt / maslowLevel).
|
|
241
|
-
*
|
|
242
|
-
* persona_questionnaire_build emitted `values` FLAT, with Schwartz and SDT
|
|
243
|
-
* merged and no higherOrder grouping — so the C4b nested-schema fix never
|
|
244
|
-
* reached the creation path, and a persona built through the questionnaire had
|
|
245
|
-
* a different value shape from the same persona loaded back. Three sites
|
|
246
|
-
* already hand-write this grouping; this is the shared one. (2026-07-29)
|
|
247
|
-
*/
|
|
248
|
-
function nestPersonaValues(values) {
|
|
249
|
-
if (!values)
|
|
250
|
-
return undefined;
|
|
251
|
-
const pick = (...keys) => {
|
|
252
|
-
const out = {};
|
|
253
|
-
for (const k of keys)
|
|
254
|
-
if (values[k] !== undefined)
|
|
255
|
-
out[k] = values[k];
|
|
256
|
-
return out;
|
|
257
|
-
};
|
|
258
|
-
return {
|
|
259
|
-
schwartz: pick("selfDirection", "stimulation", "hedonism", "achievement", "power", "security", "conformity", "tradition", "benevolence", "universalism"),
|
|
260
|
-
higherOrder: pick("openness", "selfEnhancement", "conservation", "selfTranscendence"),
|
|
261
|
-
sdt: pick("autonomyNeed", "competenceNeed", "relatednessNeed"),
|
|
262
|
-
maslowLevel: values.maslowLevel,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
function getQuestionnaireSession(sessionId) {
|
|
266
|
-
return questionnaireSessionsMap.get(sessionId);
|
|
267
|
-
}
|
|
268
|
-
function setQuestionnaireSession(sessionId, session) {
|
|
269
|
-
questionnaireSessionsMap.set(sessionId, session);
|
|
270
|
-
}
|
|
271
|
-
function clearQuestionnaireSession(sessionId) {
|
|
272
|
-
questionnaireSessionsMap.delete(sessionId);
|
|
273
|
-
}
|
|
274
|
-
function getTraitHeader(trait) {
|
|
275
|
-
const headers = {
|
|
276
|
-
patience: "Patience", riskTolerance: "Risk", comprehension: "Comprehension",
|
|
277
|
-
persistence: "Persistence", curiosity: "Curiosity", workingMemory: "Memory",
|
|
278
|
-
readingTendency: "Reading", resilience: "Resilience", selfEfficacy: "Confidence",
|
|
279
|
-
satisficing: "Decisions", trustCalibration: "Trust", interruptRecovery: "Focus",
|
|
280
|
-
informationForaging: "Search Style", changeBlindness: "Awareness", anchoringBias: "Anchoring",
|
|
281
|
-
timeHorizon: "Time Focus", attributionStyle: "Attribution", metacognitivePlanning: "Planning",
|
|
282
|
-
proceduralFluency: "Procedures", transferLearning: "Transfer", authoritySensitivity: "Authority",
|
|
283
|
-
emotionalContagion: "Emotional", fearOfMissingOut: "FOMO", socialProofSensitivity: "Social Proof",
|
|
284
|
-
mentalModelFlexibility: "Flexibility",
|
|
285
|
-
};
|
|
286
|
-
return headers[trait] || trait;
|
|
287
|
-
}
|
|
288
|
-
function convertToThirdPerson(question) {
|
|
289
|
-
return question
|
|
290
|
-
.replace(/\bdo you\b/gi, "does this persona")
|
|
291
|
-
.replace(/\bare you\b/gi, "is this persona")
|
|
292
|
-
.replace(/\byou're\b/gi, "this persona is")
|
|
293
|
-
.replace(/\byou've\b/gi, "they have")
|
|
294
|
-
.replace(/\byou'd\b/gi, "they would")
|
|
295
|
-
.replace(/\byour\b/gi, "their")
|
|
296
|
-
.replace(/\byou\b/gi, "they");
|
|
297
|
-
}
|
|
298
|
-
// Session storage (in-memory, cleared when server restarts)
|
|
299
|
-
const comparisonSessions = new Map();
|
|
300
41
|
// WCAG criteria reference for barrier mapping
|
|
301
42
|
const WCAG_CRITERIA = {
|
|
302
43
|
"1.1.1": { level: "A", description: "Non-text Content" },
|
|
@@ -346,1740 +87,164 @@ const empathyAuditSessions = new Map();
|
|
|
346
87
|
// Cleanup old sessions (older than 1 hour)
|
|
347
88
|
function cleanupOldSessions() {
|
|
348
89
|
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
|
349
|
-
for (const [id, session] of comparisonSessions) {
|
|
350
|
-
if (session.createdAt < oneHourAgo) {
|
|
351
|
-
comparisonSessions.delete(id);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
90
|
for (const [id, session] of empathyAuditSessions) {
|
|
355
|
-
if (session.createdAt < oneHourAgo) {
|
|
356
|
-
empathyAuditSessions.delete(id);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
// Helper: Get barrier hints based on persona traits
|
|
361
|
-
function getBarrierHintsForPersona(persona) {
|
|
362
|
-
const hints = [];
|
|
363
|
-
const traits = persona.accessibilityTraits;
|
|
364
|
-
if (traits?.motorControl !== undefined && traits.motorControl < 0.5) {
|
|
365
|
-
hints.push("Watch for small click targets (<44px), precise hover requirements, drag-and-drop interactions");
|
|
366
|
-
}
|
|
367
|
-
if (traits?.tremor) {
|
|
368
|
-
hints.push("Test for accidental double-clicks, cursor jitter tolerance, need for 'undo' options");
|
|
369
|
-
}
|
|
370
|
-
if (traits?.visionLevel !== undefined && traits.visionLevel < 0.5) {
|
|
371
|
-
hints.push("Check contrast ratios, text scaling support, zoom behavior at 200-300%");
|
|
372
|
-
}
|
|
373
|
-
if (traits?.colorBlindness) {
|
|
374
|
-
hints.push(`Check for color-only information (${traits.colorBlindness} colorblindness), ensure status indicators have non-color cues`);
|
|
375
|
-
}
|
|
376
|
-
if (traits?.processingSpeed !== undefined && traits.processingSpeed < 0.5) {
|
|
377
|
-
hints.push("Watch for time limits, auto-advancing content, complex multi-step processes");
|
|
378
|
-
}
|
|
379
|
-
if (traits?.attentionSpan !== undefined && traits.attentionSpan < 0.5) {
|
|
380
|
-
hints.push("Note distracting animations, long forms, lack of progress indicators");
|
|
381
|
-
}
|
|
382
|
-
// Check for hearing-related disability
|
|
383
|
-
const disabilityType = persona.disabilityType || "";
|
|
384
|
-
const personaName = persona.name || "";
|
|
385
|
-
if (disabilityType.includes("hearing") || disabilityType.includes("deaf") || personaName.includes("deaf") || personaName.includes("hearing")) {
|
|
386
|
-
hints.push("Check for audio-only content, video captions, visual alerts for audio notifications");
|
|
387
|
-
}
|
|
388
|
-
if (hints.length === 0) {
|
|
389
|
-
hints.push("Observe general usability and any unexpected difficulties");
|
|
390
|
-
}
|
|
391
|
-
return hints;
|
|
392
|
-
}
|
|
393
|
-
// Helper: Get remediation suggestion for barrier type
|
|
394
|
-
function getRemediationForBarrier(barrierType, element) {
|
|
395
|
-
const remediations = {
|
|
396
|
-
motor_precision: `Increase target size to at least 44x44px for "${element}". Add generous padding and spacing.`,
|
|
397
|
-
visual_clarity: `Improve contrast ratio to at least 4.5:1 for "${element}". Ensure text scales properly.`,
|
|
398
|
-
cognitive_load: `Simplify "${element}" - reduce options, add clear labels, provide inline help.`,
|
|
399
|
-
temporal: `Remove or extend time limits on "${element}". Allow users to pause/extend deadlines.`,
|
|
400
|
-
sensory: `Add text alternative for "${element}". Don't rely on color alone to convey information.`,
|
|
401
|
-
contrast: `Increase contrast ratio for "${element}" to at least 4.5:1 (3:1 for large text).`,
|
|
402
|
-
touch_target: `Increase touch target size for "${element}" to minimum 44x44px (WCAG 2.5.8).`,
|
|
403
|
-
timing: `Extend or remove timing constraints on "${element}". Provide pause/stop controls.`,
|
|
404
|
-
};
|
|
405
|
-
return remediations[barrierType] || `Review "${element}" for accessibility improvements.`;
|
|
406
|
-
}
|
|
407
|
-
// Helper: Derive disability type from persona traits
|
|
408
|
-
function getDisabilityTypeFromPersona(persona) {
|
|
409
|
-
const traits = persona.accessibilityTraits;
|
|
410
|
-
if (traits?.tremor)
|
|
411
|
-
return "Motor impairment (tremor)";
|
|
412
|
-
if (traits?.visionLevel !== undefined && traits.visionLevel < 0.5)
|
|
413
|
-
return "Low vision";
|
|
414
|
-
if (traits?.colorBlindness)
|
|
415
|
-
return `Color blindness (${traits.colorBlindness})`;
|
|
416
|
-
if (persona.cognitiveTraits?.workingMemory !== undefined && persona.cognitiveTraits.workingMemory < 0.5)
|
|
417
|
-
return "Cognitive (ADHD/Memory)";
|
|
418
|
-
if (traits?.processingSpeed !== undefined && traits.processingSpeed < 0.6)
|
|
419
|
-
return "Cognitive (Processing)";
|
|
420
|
-
// Fallback to name-based detection
|
|
421
|
-
if (persona.name.includes("deaf") || persona.name.includes("hearing"))
|
|
422
|
-
return "Hearing impairment";
|
|
423
|
-
if (persona.name.includes("motor"))
|
|
424
|
-
return "Motor impairment";
|
|
425
|
-
if (persona.name.includes("vision") || persona.name.includes("blind"))
|
|
426
|
-
return "Vision impairment";
|
|
427
|
-
if (persona.name.includes("cognitive") || persona.name.includes("adhd"))
|
|
428
|
-
return "Cognitive";
|
|
429
|
-
if (persona.name.includes("elderly"))
|
|
430
|
-
return "Age-related impairments";
|
|
431
|
-
if (persona.name.includes("dyslexic"))
|
|
432
|
-
return "Dyslexia";
|
|
433
|
-
return "General accessibility";
|
|
434
|
-
}
|
|
435
|
-
// Helper: Generate recommendations from empathy audit
|
|
436
|
-
function generateEmpathyRecommendations(session) {
|
|
437
|
-
const recommendations = [];
|
|
438
|
-
// Check success rate
|
|
439
|
-
const successRate = session.personaResults.filter(r => r.goalAchieved).length / session.personaResults.length;
|
|
440
|
-
if (successRate < 0.5) {
|
|
441
|
-
recommendations.push("CRITICAL: Less than 50% of disability personas could complete the goal. Fundamental accessibility improvements needed.");
|
|
442
|
-
}
|
|
443
|
-
else if (successRate < 0.8) {
|
|
444
|
-
recommendations.push("Several disability personas struggled to complete the goal. Review barriers by persona type.");
|
|
445
|
-
}
|
|
446
|
-
// Check for critical barriers
|
|
447
|
-
const criticalBarriers = session.barriers.filter(b => b.severity === "critical");
|
|
448
|
-
if (criticalBarriers.length > 0) {
|
|
449
|
-
recommendations.push(`${criticalBarriers.length} critical barriers found. Address these first as they prevent task completion.`);
|
|
450
|
-
}
|
|
451
|
-
// Check WCAG violations by level
|
|
452
|
-
const levelAViolations = Array.from(session.wcagViolations).filter(c => WCAG_CRITERIA[c]?.level === "A");
|
|
453
|
-
if (levelAViolations.length > 0) {
|
|
454
|
-
recommendations.push(`${levelAViolations.length} WCAG Level A violations (minimum compliance). These are legally required in most jurisdictions.`);
|
|
455
|
-
}
|
|
456
|
-
// Persona-specific recommendations
|
|
457
|
-
const worstPersona = session.personaResults.sort((a, b) => a.empathyScore - b.empathyScore)[0];
|
|
458
|
-
if (worstPersona && worstPersona.empathyScore < 50) {
|
|
459
|
-
recommendations.push(`"${worstPersona.persona}" (${worstPersona.disabilityType}) had the worst experience (score: ${worstPersona.empathyScore}). Prioritize improvements for this user group.`);
|
|
460
|
-
}
|
|
461
|
-
if (recommendations.length === 0) {
|
|
462
|
-
recommendations.push("Good accessibility foundation. Continue testing with real users with disabilities for deeper insights.");
|
|
463
|
-
}
|
|
464
|
-
return recommendations;
|
|
465
|
-
}
|
|
466
|
-
const collectedTools = [];
|
|
467
|
-
/**
|
|
468
|
-
* Register all CBrowser tools on an MCP server instance.
|
|
469
|
-
* Internal function - use createMcpServer() for the public API.
|
|
470
|
-
*/
|
|
471
|
-
async function registerCBrowserTools() {
|
|
472
|
-
// Auto-initialize all data directories on server start
|
|
473
|
-
ensureDirectories();
|
|
474
|
-
// Clear collected tools for fresh registration
|
|
475
|
-
collectedTools.length = 0;
|
|
476
|
-
const server = new McpServer({
|
|
477
|
-
name: "cbrowser",
|
|
478
|
-
version: VERSION,
|
|
479
|
-
});
|
|
480
|
-
// Wrap server.tool to collect tool definitions
|
|
481
|
-
const originalTool = server.tool.bind(server);
|
|
482
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
483
|
-
server.tool = (name, description, ...rest) => {
|
|
484
|
-
collectedTools.push({ name, description });
|
|
485
|
-
return originalTool(name, description, ...rest);
|
|
486
|
-
};
|
|
487
|
-
// Security layer: audit logging, description scanning, zone checks. This server
|
|
488
|
-
// registers its own ~100 tools through the deprecated `server.tool(...)` rather
|
|
489
|
-
// than through registerAllPublicTools, so the layer is attached here directly.
|
|
490
|
-
// Wiring only the shared registration path left this entire surface unaudited —
|
|
491
|
-
// found by driving a real tools/call and finding no audit entry, which is the
|
|
492
|
-
// exact "wired but never fires" outcome this change exists to eliminate.
|
|
493
|
-
applySecurityLayer(server);
|
|
494
|
-
// =========================================================================
|
|
495
|
-
// Navigation Tools
|
|
496
|
-
// =========================================================================
|
|
497
|
-
server.tool("navigate", "Navigate to a URL and take a screenshot", {
|
|
498
|
-
url: z.string().url().describe("The URL to navigate to"),
|
|
499
|
-
}, async ({ url }) => {
|
|
500
|
-
// v14.2.1: Wrap with retry for transient errors
|
|
501
|
-
return await withRetry(async () => {
|
|
502
|
-
const b = await getBrowser();
|
|
503
|
-
const result = await b.navigate(url);
|
|
504
|
-
return {
|
|
505
|
-
content: [
|
|
506
|
-
{
|
|
507
|
-
type: "text",
|
|
508
|
-
text: JSON.stringify({
|
|
509
|
-
success: true,
|
|
510
|
-
url: result.url,
|
|
511
|
-
title: result.title,
|
|
512
|
-
loadTime: result.loadTime,
|
|
513
|
-
screenshot: result.screenshot,
|
|
514
|
-
}, null, 2),
|
|
515
|
-
},
|
|
516
|
-
],
|
|
517
|
-
};
|
|
518
|
-
});
|
|
519
|
-
});
|
|
520
|
-
// =========================================================================
|
|
521
|
-
// Interaction Tools
|
|
522
|
-
// =========================================================================
|
|
523
|
-
server.tool("click", "Click an element on the page using text, selector, or description. Use verbose=true for detailed debug info on failure.", {
|
|
524
|
-
selector: z.string().describe("Element to click (text content, CSS selector, or description)"),
|
|
525
|
-
force: z.boolean().optional().describe("Bypass safety checks for destructive actions"),
|
|
526
|
-
verbose: z.boolean().optional().describe("Return available elements and AI suggestions on failure"),
|
|
527
|
-
}, async ({ selector, force, verbose }) => {
|
|
528
|
-
// v14.2.1: Wrap with retry for transient errors
|
|
529
|
-
return await withRetry(async () => {
|
|
530
|
-
const b = await getBrowser();
|
|
531
|
-
const result = await b.click(selector, { force, verbose });
|
|
532
|
-
const response = {
|
|
533
|
-
success: result.success,
|
|
534
|
-
message: result.message,
|
|
535
|
-
screenshot: result.screenshot,
|
|
536
|
-
};
|
|
537
|
-
if (result.zone)
|
|
538
|
-
response.zone = result.zone;
|
|
539
|
-
if (result.target)
|
|
540
|
-
response.target = result.target;
|
|
541
|
-
if (verbose && !result.success) {
|
|
542
|
-
if (result.availableElements)
|
|
543
|
-
response.availableElements = result.availableElements;
|
|
544
|
-
if (result.aiSuggestion)
|
|
545
|
-
response.aiSuggestion = result.aiSuggestion;
|
|
546
|
-
if (result.debugScreenshot)
|
|
547
|
-
response.debugScreenshot = result.debugScreenshot;
|
|
548
|
-
}
|
|
549
|
-
return {
|
|
550
|
-
content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
|
|
551
|
-
};
|
|
552
|
-
});
|
|
553
|
-
});
|
|
554
|
-
server.tool("smart_click", "Click with auto-retry and self-healing selectors. v11.8.0: Added confidence gating - only reports success if healed selector has >= 60% confidence.", {
|
|
555
|
-
selector: z.string().describe("Element to click"),
|
|
556
|
-
maxRetries: z.number().optional().default(3).describe("Maximum retry attempts"),
|
|
557
|
-
dismissOverlays: z.boolean().optional().default(false).describe("Dismiss overlays before clicking"),
|
|
558
|
-
}, async ({ selector, maxRetries, dismissOverlays }) => {
|
|
559
|
-
const b = await getBrowser();
|
|
560
|
-
const result = await b.smartClick(selector, { maxRetries, dismissOverlays });
|
|
561
|
-
return {
|
|
562
|
-
content: [
|
|
563
|
-
{
|
|
564
|
-
type: "text",
|
|
565
|
-
text: JSON.stringify({
|
|
566
|
-
success: result.success,
|
|
567
|
-
attempts: result.attempts.length,
|
|
568
|
-
finalSelector: result.finalSelector,
|
|
569
|
-
message: result.message,
|
|
570
|
-
aiSuggestion: result.aiSuggestion,
|
|
571
|
-
// v11.8.0: Confidence gating fields
|
|
572
|
-
confidence: result.confidence,
|
|
573
|
-
healed: result.healed,
|
|
574
|
-
healReason: result.healReason,
|
|
575
|
-
zone: result.zone,
|
|
576
|
-
}, null, 2),
|
|
577
|
-
},
|
|
578
|
-
],
|
|
579
|
-
};
|
|
580
|
-
});
|
|
581
|
-
server.tool("dismiss_overlay", "Detect and dismiss modal overlays (cookie consent, age verification, newsletter popups). Constitutional Yellow zone.", {
|
|
582
|
-
type: z.enum(["auto", "cookie", "age-verify", "newsletter", "custom"]).optional().default("auto").describe("Overlay type to detect"),
|
|
583
|
-
customSelector: z.string().optional().describe("Custom CSS selector for overlay close button"),
|
|
584
|
-
}, async ({ type, customSelector }) => {
|
|
585
|
-
const b = await getBrowser();
|
|
586
|
-
const result = await b.dismissOverlay({ type, customSelector });
|
|
587
|
-
return {
|
|
588
|
-
content: [
|
|
589
|
-
{
|
|
590
|
-
type: "text",
|
|
591
|
-
text: JSON.stringify({
|
|
592
|
-
dismissed: result.dismissed,
|
|
593
|
-
overlaysFound: result.overlaysFound,
|
|
594
|
-
overlaysDismissed: result.overlaysDismissed,
|
|
595
|
-
details: result.details,
|
|
596
|
-
suggestion: result.suggestion,
|
|
597
|
-
}, null, 2),
|
|
598
|
-
},
|
|
599
|
-
],
|
|
600
|
-
};
|
|
601
|
-
});
|
|
602
|
-
server.tool("fill", "Fill a form field with text. Use verbose=true for detailed debug info on failure.", {
|
|
603
|
-
selector: z.string().describe("Input field to fill (name, placeholder, label, or selector)"),
|
|
604
|
-
value: z.string().describe("Value to enter"),
|
|
605
|
-
verbose: z.boolean().optional().describe("Return available inputs and AI suggestions on failure"),
|
|
606
|
-
}, async ({ selector, value, verbose }) => {
|
|
607
|
-
// v14.2.1: Wrap with retry for transient errors
|
|
608
|
-
return await withRetry(async () => {
|
|
609
|
-
const b = await getBrowser();
|
|
610
|
-
const result = await b.fill(selector, value, { verbose });
|
|
611
|
-
const response = {
|
|
612
|
-
success: result.success,
|
|
613
|
-
message: result.message,
|
|
614
|
-
};
|
|
615
|
-
if (verbose && !result.success) {
|
|
616
|
-
if (result.availableInputs)
|
|
617
|
-
response.availableInputs = result.availableInputs;
|
|
618
|
-
if (result.aiSuggestion)
|
|
619
|
-
response.aiSuggestion = result.aiSuggestion;
|
|
620
|
-
if (result.debugScreenshot)
|
|
621
|
-
response.debugScreenshot = result.debugScreenshot;
|
|
622
|
-
}
|
|
623
|
-
return {
|
|
624
|
-
content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
|
|
625
|
-
};
|
|
626
|
-
});
|
|
627
|
-
});
|
|
628
|
-
server.tool("scroll", "Scroll the page in a direction. Use when content might be below the fold or to navigate long pages.", {
|
|
629
|
-
direction: z.enum(["down", "up", "top", "bottom"]).default("down").describe("Scroll direction: down (400px), up (400px), top (page start), bottom (page end)"),
|
|
630
|
-
amount: z.number().optional().describe("Custom scroll amount in pixels (only for up/down)"),
|
|
631
|
-
}, async ({ direction, amount }) => {
|
|
632
|
-
const b = await getBrowser();
|
|
633
|
-
const page = await b.getPage();
|
|
634
|
-
try {
|
|
635
|
-
const scrollAmount = amount || 400;
|
|
636
|
-
switch (direction) {
|
|
637
|
-
case "top":
|
|
638
|
-
await page.evaluate(() => window.scrollTo({ top: 0, behavior: "smooth" }));
|
|
639
|
-
break;
|
|
640
|
-
case "bottom":
|
|
641
|
-
await page.evaluate(() => window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }));
|
|
642
|
-
break;
|
|
643
|
-
case "up":
|
|
644
|
-
await page.evaluate((amt) => window.scrollBy({ top: -amt, behavior: "smooth" }), scrollAmount);
|
|
645
|
-
break;
|
|
646
|
-
case "down":
|
|
647
|
-
default:
|
|
648
|
-
await page.evaluate((amt) => window.scrollBy({ top: amt, behavior: "smooth" }), scrollAmount);
|
|
649
|
-
break;
|
|
650
|
-
}
|
|
651
|
-
// Wait for scroll animation
|
|
652
|
-
await new Promise(r => setTimeout(r, 300));
|
|
653
|
-
// Get new scroll position
|
|
654
|
-
const scrollY = await page.evaluate(() => window.scrollY);
|
|
655
|
-
const scrollHeight = await page.evaluate(() => document.body.scrollHeight);
|
|
656
|
-
const viewportHeight = await page.evaluate(() => window.innerHeight);
|
|
657
|
-
return {
|
|
658
|
-
content: [{
|
|
659
|
-
type: "text",
|
|
660
|
-
text: JSON.stringify({
|
|
661
|
-
success: true,
|
|
662
|
-
direction,
|
|
663
|
-
scrollPosition: scrollY,
|
|
664
|
-
scrollHeight,
|
|
665
|
-
viewportHeight,
|
|
666
|
-
atTop: scrollY === 0,
|
|
667
|
-
atBottom: scrollY + viewportHeight >= scrollHeight - 10,
|
|
668
|
-
}, null, 2),
|
|
669
|
-
}],
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
catch (error) {
|
|
673
|
-
return {
|
|
674
|
-
isError: true,
|
|
675
|
-
content: [{
|
|
676
|
-
type: "text",
|
|
677
|
-
text: JSON.stringify({
|
|
678
|
-
success: false,
|
|
679
|
-
error: error instanceof Error ? error.message : String(error),
|
|
680
|
-
}, null, 2),
|
|
681
|
-
}],
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
});
|
|
685
|
-
// =========================================================================
|
|
686
|
-
// Extraction Tools
|
|
687
|
-
// =========================================================================
|
|
688
|
-
server.tool("screenshot", "Take a screenshot of the current page", {
|
|
689
|
-
path: z.string().optional().describe("Optional path to save the screenshot"),
|
|
690
|
-
}, async ({ path }) => {
|
|
691
|
-
// v14.2.1: Wrap with retry for transient errors
|
|
692
|
-
return await withRetry(async () => {
|
|
693
|
-
const b = await getBrowser();
|
|
694
|
-
const file = await b.screenshot(path);
|
|
695
|
-
return {
|
|
696
|
-
content: [
|
|
697
|
-
{
|
|
698
|
-
type: "text",
|
|
699
|
-
text: JSON.stringify({ screenshot: file }, null, 2),
|
|
700
|
-
},
|
|
701
|
-
],
|
|
702
|
-
};
|
|
703
|
-
});
|
|
704
|
-
});
|
|
705
|
-
server.tool("extract", "Extract data from the page", {
|
|
706
|
-
what: z.enum(["links", "headings", "forms", "images", "text"]).describe("What to extract"),
|
|
707
|
-
}, async ({ what }) => {
|
|
708
|
-
// v14.2.1: Wrap with retry for transient errors
|
|
709
|
-
return await withRetry(async () => {
|
|
710
|
-
const b = await getBrowser();
|
|
711
|
-
const result = await b.extract(what);
|
|
712
|
-
return {
|
|
713
|
-
content: [
|
|
714
|
-
{
|
|
715
|
-
type: "text",
|
|
716
|
-
text: JSON.stringify(result.data, null, 2),
|
|
717
|
-
},
|
|
718
|
-
],
|
|
719
|
-
};
|
|
720
|
-
});
|
|
721
|
-
});
|
|
722
|
-
// =========================================================================
|
|
723
|
-
// Screen Capture Tools
|
|
724
|
-
//
|
|
725
|
-
// Same three tools as the remote server (src/mcp-tools/base/capture-tools.ts),
|
|
726
|
-
// in this server's older registration form and sharing that module's
|
|
727
|
-
// implementation — a tool that exists on only one server is half-shipped.
|
|
728
|
-
// This server is single-session, so no browser token is threaded through.
|
|
729
|
-
// =========================================================================
|
|
730
|
-
server.tool("capture_start", CAPTURE_START_DESCRIPTION, {
|
|
731
|
-
url: z.string().optional().describe("URL to navigate to before capturing. Omit to capture the current page."),
|
|
732
|
-
fps: z.number().optional().describe("Target frames per second (default: 10)"),
|
|
733
|
-
duration: z.string().optional().describe("Auto-stop after this long: 5s, 2m, 1500ms. Omit for an open-ended capture stopped by capture_stop."),
|
|
734
|
-
viewport: z.string().optional().describe("Viewport as WIDTHxHEIGHT (e.g. 1280x720) or a preset name (mobile, tablet, desktop, desktop-lg)"),
|
|
735
|
-
region: z.string().optional().describe("Capture a fixed region as x,y,width,height (e.g. 0,0,800,600). Clamped to the viewport."),
|
|
736
|
-
element: z.string().optional().describe("CSS selector to capture. Resolved to the element's bounding box once at start; the region does not follow the element if it moves."),
|
|
737
|
-
padding: z.number().optional().describe("Padding in pixels around the element region (default: 0)"),
|
|
738
|
-
format: z.string().optional().describe("Comma-separated output formats: gif,webp,mp4,webm (default: gif). mp4 needs a full ffmpeg; the bundled one does webm."),
|
|
739
|
-
quality: z.number().optional().describe("JPEG quality for captured frames, 1-100 (default: 80)"),
|
|
740
|
-
max_frames: z.number().optional().describe("Hard cap on retained frames (default: 3000)"),
|
|
741
|
-
name: z.string().optional().describe("Capture name / slug"),
|
|
742
|
-
out_dir: z.string().optional().describe("Output directory (default: a slug directory under the recordings dir)"),
|
|
743
|
-
}, async (args) => {
|
|
744
|
-
try {
|
|
745
|
-
const b = await getBrowser();
|
|
746
|
-
const payload = await startCapture(b, args);
|
|
747
|
-
return {
|
|
748
|
-
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
749
|
-
};
|
|
750
|
-
}
|
|
751
|
-
catch (error) {
|
|
752
|
-
return {
|
|
753
|
-
isError: true,
|
|
754
|
-
content: [{
|
|
755
|
-
type: "text",
|
|
756
|
-
text: JSON.stringify({
|
|
757
|
-
success: false,
|
|
758
|
-
error: error instanceof Error ? error.message : String(error),
|
|
759
|
-
}, null, 2),
|
|
760
|
-
}],
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
});
|
|
764
|
-
server.tool("capture_stop", CAPTURE_STOP_DESCRIPTION, {}, async () => {
|
|
765
|
-
try {
|
|
766
|
-
const { payload } = await stopCapture();
|
|
767
|
-
return {
|
|
768
|
-
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
catch (error) {
|
|
772
|
-
return {
|
|
773
|
-
isError: true,
|
|
774
|
-
content: [{
|
|
775
|
-
type: "text",
|
|
776
|
-
text: JSON.stringify({
|
|
777
|
-
success: false,
|
|
778
|
-
error: error instanceof Error ? error.message : String(error),
|
|
779
|
-
}, null, 2),
|
|
780
|
-
}],
|
|
781
|
-
};
|
|
782
|
-
}
|
|
783
|
-
});
|
|
784
|
-
server.tool("capture_status", CAPTURE_STATUS_DESCRIPTION, {}, async () => {
|
|
785
|
-
return {
|
|
786
|
-
content: [{ type: "text", text: JSON.stringify(captureStatus(), null, 2) }],
|
|
787
|
-
};
|
|
788
|
-
});
|
|
789
|
-
// =========================================================================
|
|
790
|
-
// Assertion Tools
|
|
791
|
-
// =========================================================================
|
|
792
|
-
server.tool("assert", "Assert a condition using natural language", {
|
|
793
|
-
assertion: z.string().describe("Natural language assertion like \"page contains 'Welcome'\" or \"title is 'Home'\""),
|
|
794
|
-
}, async ({ assertion }) => {
|
|
795
|
-
const b = await getBrowser();
|
|
796
|
-
const result = await b.assert(assertion);
|
|
797
|
-
return {
|
|
798
|
-
content: [
|
|
799
|
-
{
|
|
800
|
-
type: "text",
|
|
801
|
-
text: JSON.stringify({
|
|
802
|
-
passed: result.passed,
|
|
803
|
-
message: result.message,
|
|
804
|
-
actual: result.actual,
|
|
805
|
-
expected: result.expected,
|
|
806
|
-
}, null, 2),
|
|
807
|
-
},
|
|
808
|
-
],
|
|
809
|
-
};
|
|
810
|
-
});
|
|
811
|
-
// =========================================================================
|
|
812
|
-
// Analysis Tools
|
|
813
|
-
// =========================================================================
|
|
814
|
-
server.tool("analyze_page", "Analyze page structure for forms, buttons, links", {}, async () => {
|
|
815
|
-
const b = await getBrowser();
|
|
816
|
-
const analysis = await b.analyzePage();
|
|
817
|
-
return {
|
|
818
|
-
content: [
|
|
819
|
-
{
|
|
820
|
-
type: "text",
|
|
821
|
-
text: JSON.stringify({
|
|
822
|
-
title: analysis.title,
|
|
823
|
-
forms: analysis.forms.length,
|
|
824
|
-
buttons: analysis.buttons.length,
|
|
825
|
-
links: analysis.links.length,
|
|
826
|
-
hasLogin: analysis.hasLogin,
|
|
827
|
-
hasSearch: analysis.hasSearch,
|
|
828
|
-
hasNavigation: analysis.hasNavigation,
|
|
829
|
-
}, null, 2),
|
|
830
|
-
},
|
|
831
|
-
],
|
|
832
|
-
};
|
|
833
|
-
});
|
|
834
|
-
server.tool("generate_tests", "Generate test scenarios for a page", {
|
|
835
|
-
url: z.string().url().optional().describe("URL to analyze (uses current page if not provided)"),
|
|
836
|
-
}, async ({ url }) => {
|
|
837
|
-
const b = await getBrowser();
|
|
838
|
-
const result = await b.generateTests(url);
|
|
839
|
-
return {
|
|
840
|
-
content: [
|
|
841
|
-
{
|
|
842
|
-
type: "text",
|
|
843
|
-
text: JSON.stringify({
|
|
844
|
-
testsGenerated: result.tests.length,
|
|
845
|
-
tests: result.tests.map(t => ({
|
|
846
|
-
name: t.name,
|
|
847
|
-
description: t.description,
|
|
848
|
-
steps: t.steps.length,
|
|
849
|
-
})),
|
|
850
|
-
}, null, 2),
|
|
851
|
-
},
|
|
852
|
-
],
|
|
853
|
-
};
|
|
854
|
-
});
|
|
855
|
-
// =========================================================================
|
|
856
|
-
// Session Tools
|
|
857
|
-
// =========================================================================
|
|
858
|
-
server.tool("save_session", "Save browser session (cookies, storage) for later use", {
|
|
859
|
-
name: z.string().describe("Name for the saved session"),
|
|
860
|
-
}, async ({ name }) => {
|
|
861
|
-
const b = await getBrowser();
|
|
862
|
-
await b.saveSession(name);
|
|
863
|
-
return {
|
|
864
|
-
content: [
|
|
865
|
-
{
|
|
866
|
-
type: "text",
|
|
867
|
-
text: JSON.stringify({ success: true, sessionName: name }, null, 2),
|
|
868
|
-
},
|
|
869
|
-
],
|
|
870
|
-
};
|
|
871
|
-
});
|
|
872
|
-
server.tool("load_session", "Load a previously saved session", {
|
|
873
|
-
name: z.string().describe("Name of the session to load"),
|
|
874
|
-
}, async ({ name }) => {
|
|
875
|
-
const b = await getBrowser();
|
|
876
|
-
const result = await b.loadSession(name);
|
|
877
|
-
// v11.8.0: Return flat structure, not nested
|
|
878
|
-
return {
|
|
879
|
-
content: [
|
|
880
|
-
{
|
|
881
|
-
type: "text",
|
|
882
|
-
text: JSON.stringify(result, null, 2),
|
|
883
|
-
},
|
|
884
|
-
],
|
|
885
|
-
};
|
|
886
|
-
});
|
|
887
|
-
server.tool("list_sessions", "List all saved sessions with metadata (name, domain, cookies count, localStorage keys, created date, size)", {}, async () => {
|
|
888
|
-
const b = await getBrowser();
|
|
889
|
-
const sessions = b.listSessionsDetailed();
|
|
890
|
-
return {
|
|
891
|
-
content: [
|
|
892
|
-
{
|
|
893
|
-
type: "text",
|
|
894
|
-
text: JSON.stringify({ sessions }, null, 2),
|
|
895
|
-
},
|
|
896
|
-
],
|
|
897
|
-
};
|
|
898
|
-
});
|
|
899
|
-
server.tool("delete_session", "Delete a saved session by name", {
|
|
900
|
-
name: z.string().describe("Name of the session to delete"),
|
|
901
|
-
}, async ({ name }) => {
|
|
902
|
-
const b = await getBrowser();
|
|
903
|
-
const deleted = b.deleteSession(name);
|
|
904
|
-
return {
|
|
905
|
-
content: [
|
|
906
|
-
{
|
|
907
|
-
type: "text",
|
|
908
|
-
text: JSON.stringify({ success: deleted, name, message: deleted ? `Session '${name}' deleted` : `Session '${name}' not found` }),
|
|
909
|
-
},
|
|
910
|
-
],
|
|
911
|
-
};
|
|
912
|
-
});
|
|
913
|
-
// =========================================================================
|
|
914
|
-
// Self-Healing Tools
|
|
915
|
-
// =========================================================================
|
|
916
|
-
server.tool("heal_stats", "Get self-healing selector cache statistics", {}, async () => {
|
|
917
|
-
// v14.2.1: Wrap with retry for transient errors
|
|
918
|
-
return await withRetry(async () => {
|
|
919
|
-
const b = await getBrowser();
|
|
920
|
-
const stats = b.getSelectorCacheStats();
|
|
921
|
-
return {
|
|
922
|
-
content: [
|
|
923
|
-
{
|
|
924
|
-
type: "text",
|
|
925
|
-
text: JSON.stringify(stats, null, 2),
|
|
926
|
-
},
|
|
927
|
-
],
|
|
928
|
-
};
|
|
929
|
-
});
|
|
930
|
-
});
|
|
931
|
-
// =========================================================================
|
|
932
|
-
// Visual Testing Tools (v7.0.0+)
|
|
933
|
-
// =========================================================================
|
|
934
|
-
server.tool("visual_baseline", "Capture a visual baseline for a URL", {
|
|
935
|
-
url: z.string().url().describe("URL to capture baseline for"),
|
|
936
|
-
name: z.string().describe("Name for the baseline"),
|
|
937
|
-
}, async ({ url, name }) => {
|
|
938
|
-
const result = await captureVisualBaseline(url, name, {});
|
|
939
|
-
return {
|
|
940
|
-
content: [
|
|
941
|
-
{
|
|
942
|
-
type: "text",
|
|
943
|
-
text: JSON.stringify({
|
|
944
|
-
success: true,
|
|
945
|
-
name: result.name,
|
|
946
|
-
url: result.url,
|
|
947
|
-
timestamp: result.timestamp,
|
|
948
|
-
}, null, 2),
|
|
949
|
-
},
|
|
950
|
-
],
|
|
951
|
-
};
|
|
952
|
-
});
|
|
953
|
-
server.tool("visual_regression", "Run AI visual regression test against a baseline", {
|
|
954
|
-
url: z.string().url().describe("URL to test"),
|
|
955
|
-
baselineName: z.string().describe("Name of baseline to compare against"),
|
|
956
|
-
}, async ({ url, baselineName }) => {
|
|
957
|
-
const result = await runVisualRegression(url, baselineName);
|
|
958
|
-
return {
|
|
959
|
-
content: [
|
|
960
|
-
{
|
|
961
|
-
type: "text",
|
|
962
|
-
text: JSON.stringify({
|
|
963
|
-
passed: result.passed,
|
|
964
|
-
similarityScore: result.analysis?.similarityScore,
|
|
965
|
-
...(result.analysis?.wassersteinDistance !== undefined ? { wassersteinDistance: result.analysis.wassersteinDistance } : {}),
|
|
966
|
-
status: result.analysis?.overallStatus,
|
|
967
|
-
summary: result.analysis?.summary,
|
|
968
|
-
changes: result.analysis?.changes?.length || 0,
|
|
969
|
-
}, null, 2),
|
|
970
|
-
},
|
|
971
|
-
],
|
|
972
|
-
};
|
|
973
|
-
});
|
|
974
|
-
server.tool("cross_browser_test", "Test page rendering across multiple browsers", {
|
|
975
|
-
url: z.string().url().describe("URL to test"),
|
|
976
|
-
browsers: z.array(z.enum(["chromium", "firefox", "webkit"])).optional().describe("Browsers to test"),
|
|
977
|
-
}, async ({ url, browsers }) => {
|
|
978
|
-
const result = await runCrossBrowserTest(url, { browsers });
|
|
979
|
-
return {
|
|
980
|
-
content: [
|
|
981
|
-
{
|
|
982
|
-
type: "text",
|
|
983
|
-
text: JSON.stringify({
|
|
984
|
-
url: result.url,
|
|
985
|
-
overallStatus: result.overallStatus,
|
|
986
|
-
summary: result.summary,
|
|
987
|
-
// The evidence used to be collapsed to two integers —
|
|
988
|
-
// screenshotCount / comparisonCount — while `result` carried every
|
|
989
|
-
// screenshot path, every pairwise similarity score, and
|
|
990
|
-
// problematicBrowsers. So the caller received an absolute verdict
|
|
991
|
-
// ("renders consistently across all tested browsers") with every
|
|
992
|
-
// number that could falsify it removed, and no way to open the
|
|
993
|
-
// screenshots it said it had taken.
|
|
994
|
-
//
|
|
995
|
-
// It also explains the contradiction with cross_browser_diff: this
|
|
996
|
-
// tool compares ABOVE-THE-FOLD PIXELS at a forced 1920x1080, while
|
|
997
|
-
// the diff compares FULL-PAGE TEXT at 1280x720. Two different
|
|
998
|
-
// measurements reported in language that claims to settle the same
|
|
999
|
-
// question. `scope` now says which one you are reading.
|
|
1000
|
-
// (2026-07-29)
|
|
1001
|
-
scope: "above-the-fold pixels at 1920x1080; page TEXT is not compared — use cross_browser_diff for content",
|
|
1002
|
-
screenshots: result.screenshots.map((s) => ({
|
|
1003
|
-
browser: s.browser,
|
|
1004
|
-
path: s.screenshotPath,
|
|
1005
|
-
viewport: s.viewport,
|
|
1006
|
-
})),
|
|
1007
|
-
comparisons: result.comparisons.map((c) => ({
|
|
1008
|
-
browserA: c.browserA,
|
|
1009
|
-
browserB: c.browserB,
|
|
1010
|
-
status: c.analysis?.overallStatus,
|
|
1011
|
-
similarity: c.analysis?.similarityScore,
|
|
1012
|
-
...(c.analysis?.wassersteinDistance !== undefined ? { wassersteinDistance: c.analysis.wassersteinDistance } : {}),
|
|
1013
|
-
changes: (c.analysis?.changes ?? []).map((ch) => ({
|
|
1014
|
-
severity: ch.severity,
|
|
1015
|
-
description: ch.description,
|
|
1016
|
-
})),
|
|
1017
|
-
})),
|
|
1018
|
-
...(result.problematicBrowsers?.length ? { problematicBrowsers: result.problematicBrowsers } : {}),
|
|
1019
|
-
screenshotCount: result.screenshots.length,
|
|
1020
|
-
comparisonCount: result.comparisons.length,
|
|
1021
|
-
...(result.missingBrowsers?.length ? { missingBrowsers: result.missingBrowsers } : {}),
|
|
1022
|
-
...(result.availableBrowsers ? { availableBrowsers: result.availableBrowsers } : {}),
|
|
1023
|
-
...(result.suggestion ? { suggestion: result.suggestion } : {}),
|
|
1024
|
-
}, null, 2),
|
|
1025
|
-
},
|
|
1026
|
-
],
|
|
1027
|
-
};
|
|
1028
|
-
});
|
|
1029
|
-
server.tool("cross_browser_diff", "Quick diff of page metrics across browsers", {
|
|
1030
|
-
url: z.string().url().describe("URL to compare"),
|
|
1031
|
-
browsers: z.array(z.enum(["chromium", "firefox", "webkit"])).optional().describe("Browsers to compare"),
|
|
1032
|
-
}, async ({ url, browsers }) => {
|
|
1033
|
-
const result = await crossBrowserDiff(url, browsers);
|
|
1034
|
-
return {
|
|
1035
|
-
content: [
|
|
1036
|
-
{
|
|
1037
|
-
type: "text",
|
|
1038
|
-
text: JSON.stringify({
|
|
1039
|
-
url: result.url,
|
|
1040
|
-
browsers: result.browsers,
|
|
1041
|
-
differences: result.differences,
|
|
1042
|
-
metrics: result.metrics,
|
|
1043
|
-
...(result.missingBrowsers?.length ? { missingBrowsers: result.missingBrowsers } : {}),
|
|
1044
|
-
...(result.availableBrowsers ? { availableBrowsers: result.availableBrowsers } : {}),
|
|
1045
|
-
...(result.suggestion ? { suggestion: result.suggestion } : {}),
|
|
1046
|
-
}, null, 2),
|
|
1047
|
-
},
|
|
1048
|
-
],
|
|
1049
|
-
};
|
|
1050
|
-
});
|
|
1051
|
-
server.tool("responsive_test", "Test page across different viewport sizes", {
|
|
1052
|
-
url: z.string().url().describe("URL to test"),
|
|
1053
|
-
viewports: z.array(z.string()).optional().describe("Viewport presets (mobile, tablet, desktop, etc.)"),
|
|
1054
|
-
}, async ({ url, viewports }) => {
|
|
1055
|
-
const result = await runResponsiveTest(url, { viewports });
|
|
1056
|
-
return {
|
|
1057
|
-
content: [
|
|
1058
|
-
{
|
|
1059
|
-
type: "text",
|
|
1060
|
-
text: JSON.stringify({
|
|
1061
|
-
url: result.url,
|
|
1062
|
-
overallStatus: result.overallStatus,
|
|
1063
|
-
summary: result.summary,
|
|
1064
|
-
// `result.issues` and `problematicViewports` were both computed and
|
|
1065
|
-
// then dropped here, so the caller got "13 issues: 7 overflow, 2
|
|
1066
|
-
// unreadable text..." with no way to learn WHICH viewport or WHICH
|
|
1067
|
-
// element — a count they could not act on. Same computed-then-
|
|
1068
|
-
// discarded shape as the cross-browser handler above. (2026-07-29)
|
|
1069
|
-
issues: (result.issues ?? []).map((i) => ({
|
|
1070
|
-
type: i.type,
|
|
1071
|
-
severity: i.severity,
|
|
1072
|
-
description: i.description,
|
|
1073
|
-
affectedViewports: i.affectedViewports,
|
|
1074
|
-
...(i.breakpointRange ? { breakpointRange: i.breakpointRange } : {}),
|
|
1075
|
-
})),
|
|
1076
|
-
...(result.problematicViewports?.length ? { problematicViewports: result.problematicViewports } : {}),
|
|
1077
|
-
viewports: result.screenshots.map((s) => ({
|
|
1078
|
-
name: s.viewport
|
|
1079
|
-
?? s.name,
|
|
1080
|
-
path: s.screenshotPath,
|
|
1081
|
-
})),
|
|
1082
|
-
viewportsCount: result.screenshots.length,
|
|
1083
|
-
}, null, 2),
|
|
1084
|
-
},
|
|
1085
|
-
],
|
|
1086
|
-
};
|
|
1087
|
-
});
|
|
1088
|
-
server.tool("ab_comparison", "Compare two URLs visually (staging vs production)", {
|
|
1089
|
-
urlA: z.string().url().describe("First URL (e.g., staging)"),
|
|
1090
|
-
urlB: z.string().url().describe("Second URL (e.g., production)"),
|
|
1091
|
-
labelA: z.string().optional().describe("Label for first URL"),
|
|
1092
|
-
labelB: z.string().optional().describe("Label for second URL"),
|
|
1093
|
-
}, async ({ urlA, urlB, labelA, labelB }) => {
|
|
1094
|
-
const labels = labelA && labelB ? { a: labelA, b: labelB } : undefined;
|
|
1095
|
-
const result = await runABComparison(urlA, urlB, { labels });
|
|
1096
|
-
return {
|
|
1097
|
-
content: [
|
|
1098
|
-
{
|
|
1099
|
-
type: "text",
|
|
1100
|
-
// v11.11.0: Include full differences for structured diff (stress test fix)
|
|
1101
|
-
text: JSON.stringify({
|
|
1102
|
-
overallStatus: result.overallStatus,
|
|
1103
|
-
similarityScore: result.analysis?.similarityScore,
|
|
1104
|
-
...(result.analysis?.wassersteinDistance !== undefined ? { wassersteinDistance: result.analysis.wassersteinDistance } : {}),
|
|
1105
|
-
summary: result.summary,
|
|
1106
|
-
// v11.11.0: Return detailed differences instead of just count
|
|
1107
|
-
differences: result.differences.slice(0, 10).map(d => ({
|
|
1108
|
-
type: d.type,
|
|
1109
|
-
severity: d.severity,
|
|
1110
|
-
description: d.description,
|
|
1111
|
-
affectedSide: d.affectedSide,
|
|
1112
|
-
})),
|
|
1113
|
-
differenceCount: result.differences.length,
|
|
1114
|
-
// v11.11.0: Include page structure comparison summary
|
|
1115
|
-
structureSummary: {
|
|
1116
|
-
a: {
|
|
1117
|
-
headings: result.screenshots.a.structure?.headings?.length || 0,
|
|
1118
|
-
links: result.screenshots.a.structure?.links?.length || 0,
|
|
1119
|
-
forms: result.screenshots.a.structure?.forms || 0,
|
|
1120
|
-
buttons: result.screenshots.a.structure?.buttons?.length || 0,
|
|
1121
|
-
},
|
|
1122
|
-
b: {
|
|
1123
|
-
headings: result.screenshots.b.structure?.headings?.length || 0,
|
|
1124
|
-
links: result.screenshots.b.structure?.links?.length || 0,
|
|
1125
|
-
forms: result.screenshots.b.structure?.forms || 0,
|
|
1126
|
-
buttons: result.screenshots.b.structure?.buttons?.length || 0,
|
|
1127
|
-
},
|
|
1128
|
-
},
|
|
1129
|
-
duration: result.duration,
|
|
1130
|
-
}, null, 2),
|
|
1131
|
-
},
|
|
1132
|
-
],
|
|
1133
|
-
};
|
|
1134
|
-
});
|
|
1135
|
-
// =========================================================================
|
|
1136
|
-
// Testing Tools (v6.0.0+)
|
|
1137
|
-
// =========================================================================
|
|
1138
|
-
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.", {
|
|
1139
|
-
filepath: z.string().describe("Path to the test file"),
|
|
1140
|
-
dryRun: z.boolean().optional().describe("Parse and display steps without executing"),
|
|
1141
|
-
fuzzyMatch: z.boolean().optional().describe("Use case-insensitive fuzzy matching for assertions"),
|
|
1142
|
-
}, async ({ filepath, dryRun, fuzzyMatch }) => {
|
|
1143
|
-
const fs = await import("fs");
|
|
1144
|
-
if (!fs.existsSync(filepath)) {
|
|
1145
|
-
return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: `Test file not found: ${filepath}` }) }] };
|
|
1146
|
-
}
|
|
1147
|
-
const fileContent = fs.readFileSync(filepath, "utf-8");
|
|
1148
|
-
const suiteName = filepath.split("/").pop()?.replace(/\.[^.]+$/, "") || "Test Suite";
|
|
1149
|
-
const suite = parseNLTestSuite(fileContent, suiteName);
|
|
1150
|
-
if (dryRun) {
|
|
1151
|
-
const dryResult = dryRunNLTestSuite(suite);
|
|
1152
|
-
return { content: [{ type: "text", text: JSON.stringify(dryResult, null, 2) }] };
|
|
1153
|
-
}
|
|
1154
|
-
const result = await runNLTestSuite(suite, { fuzzyMatch: fuzzyMatch || false });
|
|
1155
|
-
return {
|
|
1156
|
-
content: [
|
|
1157
|
-
{
|
|
1158
|
-
type: "text",
|
|
1159
|
-
text: JSON.stringify({
|
|
1160
|
-
name: result.name,
|
|
1161
|
-
total: result.summary.total,
|
|
1162
|
-
passed: result.summary.passed,
|
|
1163
|
-
failed: result.summary.failed,
|
|
1164
|
-
passRate: `${result.summary.passRate.toFixed(1)}%`,
|
|
1165
|
-
// v11.6.0: Step-level statistics for better granularity
|
|
1166
|
-
totalSteps: result.summary.totalSteps,
|
|
1167
|
-
passedSteps: result.summary.passedSteps,
|
|
1168
|
-
failedSteps: result.summary.failedSteps,
|
|
1169
|
-
stepPassRate: result.summary.stepPassRate ? `${result.summary.stepPassRate.toFixed(1)}%` : undefined,
|
|
1170
|
-
duration: result.duration,
|
|
1171
|
-
recommendations: result.recommendations,
|
|
1172
|
-
testResults: result.testResults.map(t => ({
|
|
1173
|
-
name: t.name,
|
|
1174
|
-
passed: t.passed,
|
|
1175
|
-
duration: t.duration,
|
|
1176
|
-
error: t.error,
|
|
1177
|
-
steps: t.stepResults.map(s => ({
|
|
1178
|
-
instruction: s.instruction,
|
|
1179
|
-
parsed: s.parsed,
|
|
1180
|
-
passed: s.passed,
|
|
1181
|
-
duration: s.duration,
|
|
1182
|
-
error: s.error,
|
|
1183
|
-
actualValue: s.actualValue,
|
|
1184
|
-
})),
|
|
1185
|
-
})),
|
|
1186
|
-
}, null, 2),
|
|
1187
|
-
},
|
|
1188
|
-
],
|
|
1189
|
-
};
|
|
1190
|
-
});
|
|
1191
|
-
server.tool("nl_test_inline", "Run natural language tests from inline content. Returns step-level results with enriched error info, partial matches, and suggestions.", {
|
|
1192
|
-
content: z.string().describe("Test content with instructions like 'go to https://...' and 'click login'"),
|
|
1193
|
-
name: z.string().optional().describe("Name for the test suite"),
|
|
1194
|
-
dryRun: z.boolean().optional().describe("Parse and display steps without executing"),
|
|
1195
|
-
fuzzyMatch: z.boolean().optional().describe("Use case-insensitive fuzzy matching for assertions"),
|
|
1196
|
-
}, async ({ content, name, dryRun, fuzzyMatch }) => {
|
|
1197
|
-
const suite = parseNLTestSuite(content, name || "Inline Test");
|
|
1198
|
-
if (dryRun) {
|
|
1199
|
-
const dryResult = dryRunNLTestSuite(suite);
|
|
1200
|
-
return { content: [{ type: "text", text: JSON.stringify(dryResult, null, 2) }] };
|
|
1201
|
-
}
|
|
1202
|
-
const result = await runNLTestSuite(suite, { fuzzyMatch: fuzzyMatch || false });
|
|
1203
|
-
return {
|
|
1204
|
-
content: [
|
|
1205
|
-
{
|
|
1206
|
-
type: "text",
|
|
1207
|
-
text: JSON.stringify({
|
|
1208
|
-
name: result.name,
|
|
1209
|
-
total: result.summary.total,
|
|
1210
|
-
passed: result.summary.passed,
|
|
1211
|
-
failed: result.summary.failed,
|
|
1212
|
-
passRate: `${result.summary.passRate.toFixed(1)}%`,
|
|
1213
|
-
// v11.6.0: Step-level statistics for better granularity
|
|
1214
|
-
totalSteps: result.summary.totalSteps,
|
|
1215
|
-
passedSteps: result.summary.passedSteps,
|
|
1216
|
-
failedSteps: result.summary.failedSteps,
|
|
1217
|
-
stepPassRate: result.summary.stepPassRate ? `${result.summary.stepPassRate.toFixed(1)}%` : undefined,
|
|
1218
|
-
duration: result.duration,
|
|
1219
|
-
recommendations: result.recommendations,
|
|
1220
|
-
testResults: result.testResults.map(t => ({
|
|
1221
|
-
name: t.name,
|
|
1222
|
-
passed: t.passed,
|
|
1223
|
-
duration: t.duration,
|
|
1224
|
-
error: t.error,
|
|
1225
|
-
steps: t.stepResults.map(s => ({
|
|
1226
|
-
instruction: s.instruction,
|
|
1227
|
-
parsed: s.parsed,
|
|
1228
|
-
passed: s.passed,
|
|
1229
|
-
duration: s.duration,
|
|
1230
|
-
error: s.error,
|
|
1231
|
-
actualValue: s.actualValue,
|
|
1232
|
-
})),
|
|
1233
|
-
})),
|
|
1234
|
-
}, null, 2),
|
|
1235
|
-
},
|
|
1236
|
-
],
|
|
1237
|
-
};
|
|
1238
|
-
});
|
|
1239
|
-
server.tool("repair_test", "AI-powered test repair for broken tests", {
|
|
1240
|
-
testName: z.string().describe("Name for the test"),
|
|
1241
|
-
steps: z.array(z.string()).describe("Test step instructions"),
|
|
1242
|
-
autoApply: z.boolean().optional().describe("Automatically apply repairs"),
|
|
1243
|
-
}, async ({ testName, steps, autoApply }) => {
|
|
1244
|
-
const testCase = {
|
|
1245
|
-
name: testName,
|
|
1246
|
-
steps: steps.map(instruction => ({
|
|
1247
|
-
instruction,
|
|
1248
|
-
action: "unknown",
|
|
1249
|
-
})),
|
|
1250
|
-
};
|
|
1251
|
-
const result = await repairTest(testCase, { autoApply: autoApply || false });
|
|
1252
|
-
return {
|
|
1253
|
-
content: [
|
|
1254
|
-
{
|
|
1255
|
-
type: "text",
|
|
1256
|
-
text: JSON.stringify({
|
|
1257
|
-
originalTest: result.originalTest.name,
|
|
1258
|
-
failedSteps: result.failedSteps,
|
|
1259
|
-
repairedSteps: result.repairedSteps,
|
|
1260
|
-
repairedTestPasses: result.repairedTestPasses,
|
|
1261
|
-
repairs: result.failureAnalyses.map(a => ({
|
|
1262
|
-
step: a.step.instruction,
|
|
1263
|
-
error: a.error,
|
|
1264
|
-
suggestion: a.suggestions[0]?.suggestedInstruction || "No suggestion",
|
|
1265
|
-
})),
|
|
1266
|
-
}, null, 2),
|
|
1267
|
-
},
|
|
1268
|
-
],
|
|
1269
|
-
};
|
|
1270
|
-
});
|
|
1271
|
-
server.tool("detect_flaky_tests", "Detect flaky/unreliable tests by running multiple times", {
|
|
1272
|
-
testContent: z.string().describe("Test content to analyze"),
|
|
1273
|
-
runs: z.number().optional().default(5).describe("Number of times to run each test"),
|
|
1274
|
-
threshold: z.number().optional().default(20).describe("Flakiness threshold percentage"),
|
|
1275
|
-
}, async ({ testContent, runs, threshold }) => {
|
|
1276
|
-
const suite = parseNLTestSuite(testContent, "Flaky Test Analysis");
|
|
1277
|
-
const result = await detectFlakyTests(suite, { runs, flakinessThreshold: threshold });
|
|
1278
|
-
return {
|
|
1279
|
-
content: [
|
|
1280
|
-
{
|
|
1281
|
-
type: "text",
|
|
1282
|
-
text: JSON.stringify({
|
|
1283
|
-
suiteName: result.suiteName,
|
|
1284
|
-
totalTests: result.summary.totalTests,
|
|
1285
|
-
stablePass: result.summary.stablePassTests,
|
|
1286
|
-
stableFail: result.summary.stableFailTests,
|
|
1287
|
-
flakyTests: result.summary.flakyTests,
|
|
1288
|
-
overallFlakiness: `${result.summary.overallFlakinessScore.toFixed(1)}%`,
|
|
1289
|
-
analyses: result.testAnalyses.map(a => ({
|
|
1290
|
-
test: a.testName,
|
|
1291
|
-
classification: a.classification,
|
|
1292
|
-
passRate: `${((a.passCount / a.totalRuns) * 100).toFixed(0)}%`,
|
|
1293
|
-
flakiness: `${a.flakinessScore}%`,
|
|
1294
|
-
})),
|
|
1295
|
-
}, null, 2),
|
|
1296
|
-
},
|
|
1297
|
-
],
|
|
1298
|
-
};
|
|
1299
|
-
});
|
|
1300
|
-
server.tool("coverage_map", "Generate test coverage map for a site", {
|
|
1301
|
-
baseUrl: z.string().url().describe("Base URL to analyze"),
|
|
1302
|
-
testFiles: z.array(z.string()).describe("Array of test file paths"),
|
|
1303
|
-
maxPages: z.number().optional().default(100).describe("Maximum pages to crawl"),
|
|
1304
|
-
}, async ({ baseUrl, testFiles, maxPages }) => {
|
|
1305
|
-
const result = await generateCoverageMap(baseUrl, testFiles, { maxPages });
|
|
1306
|
-
// Mirrors the guard in mcp-tools/base/testing-tools.ts. This file is the
|
|
1307
|
-
// local stdio server's independent registration of the same tool, so a fix
|
|
1308
|
-
// applied to only one of the two surfaces leaves the bug live on the other.
|
|
1309
|
-
if (result.missingTestFiles.length === testFiles.length && testFiles.length > 0) {
|
|
1310
|
-
return {
|
|
1311
|
-
isError: true,
|
|
1312
|
-
content: [{
|
|
1313
|
-
type: "text",
|
|
1314
|
-
text: JSON.stringify({
|
|
1315
|
-
error: "no_readable_test_files",
|
|
1316
|
-
message: `None of the ${testFiles.length} test file path(s) could be read, so there is no coverage to report.`,
|
|
1317
|
-
missingTestFiles: result.missingTestFiles,
|
|
1318
|
-
}, null, 2),
|
|
1319
|
-
}],
|
|
1320
|
-
};
|
|
1321
|
-
}
|
|
1322
|
-
return {
|
|
1323
|
-
content: [
|
|
1324
|
-
{
|
|
1325
|
-
type: "text",
|
|
1326
|
-
text: JSON.stringify({
|
|
1327
|
-
totalPages: result.sitePages.length,
|
|
1328
|
-
testedPages: result.testedPages.length,
|
|
1329
|
-
untestedPages: result.analysis.untestedPages,
|
|
1330
|
-
overallCoverage: `${result.analysis.coveragePercent.toFixed(1)}%`,
|
|
1331
|
-
...(result.missingTestFiles.length > 0 ? {
|
|
1332
|
-
missingTestFiles: result.missingTestFiles,
|
|
1333
|
-
warning: `${result.missingTestFiles.length} of ${testFiles.length} test files could not be read and contributed no coverage.`,
|
|
1334
|
-
} : {}),
|
|
1335
|
-
gaps: result.gaps.slice(0, 10).map(g => ({
|
|
1336
|
-
url: g.page.url,
|
|
1337
|
-
priority: g.priority,
|
|
1338
|
-
reason: g.reason,
|
|
1339
|
-
})),
|
|
1340
|
-
}, null, 2),
|
|
1341
|
-
},
|
|
1342
|
-
],
|
|
1343
|
-
};
|
|
1344
|
-
});
|
|
1345
|
-
// =========================================================================
|
|
1346
|
-
// Analysis Tools (v4.0.0+)
|
|
1347
|
-
// =========================================================================
|
|
1348
|
-
server.tool("hunt_bugs", "Autonomous bug hunting - crawl and find issues. Returns bugs with severity, selector, and actionable recommendation for each issue found.", {
|
|
1349
|
-
url: z.string().url().describe("Starting URL to hunt from"),
|
|
1350
|
-
maxPages: z.number().optional().default(10).describe("Maximum pages to visit"),
|
|
1351
|
-
limit: z.number().optional().default(25).describe("How many bugs to return, worst first. The full count and a per-page/per-type breakdown are always reported, so a truncated list never hides the shape of what was found."),
|
|
1352
|
-
offset: z.number().optional().default(0).describe("Skip this many bugs before returning, for paging through a large result."),
|
|
1353
|
-
timeout: z.number().optional().default(60000).describe("Timeout in milliseconds"),
|
|
1354
|
-
}, async ({ url, maxPages, timeout, limit, offset }) => {
|
|
1355
|
-
const b = await getBrowser();
|
|
1356
|
-
const result = await huntBugs(b, url, { maxPages, timeout });
|
|
1357
|
-
// Same response as the HTTP tool. This copy returned the first ten bugs
|
|
1358
|
-
// in crawl order, with no severity sort, paging or breakdowns.
|
|
1359
|
-
return {
|
|
1360
|
-
content: [
|
|
1361
|
-
{
|
|
1362
|
-
type: "text",
|
|
1363
|
-
text: JSON.stringify(huntBugsResponse(result, { limit, offset }), null, 2),
|
|
1364
|
-
},
|
|
1365
|
-
],
|
|
1366
|
-
};
|
|
1367
|
-
});
|
|
1368
|
-
server.tool("chaos_test", "Inject failures and test resilience", {
|
|
1369
|
-
url: z.string().url().describe("URL to test"),
|
|
1370
|
-
networkLatency: z.number().optional().describe("Simulate network latency (ms)"),
|
|
1371
|
-
offline: z.boolean().optional().describe("Simulate offline mode"),
|
|
1372
|
-
blockUrls: z.array(z.string()).optional().describe("URL patterns to block"),
|
|
1373
|
-
}, async ({ url, networkLatency, offline, blockUrls }) => {
|
|
1374
|
-
const b = await getBrowser();
|
|
1375
|
-
try {
|
|
1376
|
-
const result = await runChaosTest(b, url, { networkLatency, offline, blockUrls });
|
|
1377
|
-
return {
|
|
1378
|
-
content: [
|
|
1379
|
-
{
|
|
1380
|
-
type: "text",
|
|
1381
|
-
text: JSON.stringify({
|
|
1382
|
-
passed: result.passed,
|
|
1383
|
-
errors: result.errors,
|
|
1384
|
-
duration: result.duration,
|
|
1385
|
-
// v16.11.0: Include impact analysis in response
|
|
1386
|
-
impact: result.impact,
|
|
1387
|
-
}, null, 2),
|
|
1388
|
-
},
|
|
1389
|
-
],
|
|
1390
|
-
};
|
|
1391
|
-
}
|
|
1392
|
-
catch (error) {
|
|
1393
|
-
// v16.11.0: Graceful error handling for chaos test crashes
|
|
1394
|
-
// Attempt browser recovery to prevent server crash
|
|
1395
|
-
try {
|
|
1396
|
-
await b.recoverBrowser();
|
|
1397
|
-
}
|
|
1398
|
-
catch {
|
|
1399
|
-
// Browser recovery failed, but continue with error response
|
|
1400
|
-
}
|
|
1401
|
-
return {
|
|
1402
|
-
content: [
|
|
1403
|
-
{
|
|
1404
|
-
type: "text",
|
|
1405
|
-
text: JSON.stringify({
|
|
1406
|
-
passed: false,
|
|
1407
|
-
errors: [`Chaos test crashed: ${error.message}`],
|
|
1408
|
-
duration: 0,
|
|
1409
|
-
impact: {
|
|
1410
|
-
loadTimeMs: 0,
|
|
1411
|
-
blockedResources: [],
|
|
1412
|
-
failedResources: [],
|
|
1413
|
-
delayedResources: [],
|
|
1414
|
-
pageCompleted: false,
|
|
1415
|
-
pageInteractive: false,
|
|
1416
|
-
consoleErrors: 0,
|
|
1417
|
-
degradationSummary: ["Test crashed - browser recovered"],
|
|
1418
|
-
},
|
|
1419
|
-
recovered: true,
|
|
1420
|
-
}, null, 2),
|
|
1421
|
-
},
|
|
1422
|
-
],
|
|
1423
|
-
};
|
|
1424
|
-
}
|
|
1425
|
-
});
|
|
1426
|
-
server.tool("find_element_by_intent", FIND_ELEMENT_BY_INTENT_DESCRIPTION, {
|
|
1427
|
-
intent: z.string().describe("Natural language description like 'sign up button in the header', 'email field', 'the cheapest product', 'login form', 'footer navigation'"),
|
|
1428
|
-
verbose: z.boolean().optional().describe("On a miss, list the first visible links/buttons/fields with unique selectors so you can rephrase; on a hit, unchanged"),
|
|
1429
|
-
}, async ({ intent, verbose }) => {
|
|
1430
|
-
const b = await getBrowser();
|
|
1431
|
-
const result = await findElementByIntent(b, intent, { verbose });
|
|
1432
|
-
if (result && result.confidence > 0) {
|
|
1433
|
-
return {
|
|
1434
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1435
|
-
};
|
|
1436
|
-
}
|
|
1437
|
-
// No match or zero-confidence verbose result
|
|
1438
|
-
return {
|
|
1439
|
-
content: [{ type: "text", text: JSON.stringify(result || { found: false, message: "No matching element found" }, null, 2) }],
|
|
1440
|
-
};
|
|
1441
|
-
});
|
|
1442
|
-
// =========================================================================
|
|
1443
|
-
// Cognitive Simulation Tools (v8.3.0)
|
|
1444
|
-
// =========================================================================
|
|
1445
|
-
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.", {
|
|
1446
|
-
persona: z.string().describe("Persona name (e.g., 'first-timer', 'elderly-user', 'power-user') or custom description"),
|
|
1447
|
-
goal: z.string().describe("What the simulated user is trying to accomplish"),
|
|
1448
|
-
startUrl: z.string().url().describe("Starting URL for the journey"),
|
|
1449
|
-
customTraits: z.object({
|
|
1450
|
-
// Core 7 traits
|
|
1451
|
-
patience: z.number().min(0).max(1).optional().describe("How long before giving up (0=impatient, 1=very patient)"),
|
|
1452
|
-
riskTolerance: z.number().min(0).max(1).optional().describe("Willingness to click unfamiliar elements (0=cautious, 1=adventurous)"),
|
|
1453
|
-
comprehension: z.number().min(0).max(1).optional().describe("UI/UX understanding speed (0=confused easily, 1=grasps quickly)"),
|
|
1454
|
-
persistence: z.number().min(0).max(1).optional().describe("Retry same approach vs try different (0=explores, 1=persists)"),
|
|
1455
|
-
curiosity: z.number().min(0).max(1).optional().describe("Tendency to explore vs stay focused (0=focused, 1=exploratory)"),
|
|
1456
|
-
workingMemory: z.number().min(0).max(1).optional().describe("Remembers what was tried (0=forgets, 1=remembers all)"),
|
|
1457
|
-
readingTendency: z.number().min(0).max(1).optional().describe("Reads content vs scans for CTAs (0=scans, 1=reads everything)"),
|
|
1458
|
-
// v16.11.0: Extended traits (18 more = 25 total)
|
|
1459
|
-
resilience: z.number().min(0).max(1).optional().describe("Emotional recovery after setbacks (0=gives up, 1=bounces back)"),
|
|
1460
|
-
selfEfficacy: z.number().min(0).max(1).optional().describe("Belief in ability to complete task (0=doubts self, 1=confident)"),
|
|
1461
|
-
satisficing: z.number().min(0).max(1).optional().describe("Accepts 'good enough' vs seeks optimal (0=optimizes, 1=satisfices)"),
|
|
1462
|
-
trustCalibration: z.number().min(0).max(1).optional().describe("Trust in website/CTAs (0=skeptical, 1=trusting)"),
|
|
1463
|
-
interruptRecovery: z.number().min(0).max(1).optional().describe("Recovers from distractions (0=loses place, 1=resumes smoothly)"),
|
|
1464
|
-
informationForaging: z.number().min(0).max(1).optional().describe("Efficiency finding info (0=scattered, 1=systematic)"),
|
|
1465
|
-
changeBlindness: z.number().min(0).max(1).optional().describe("Notices UI changes (0=misses changes, 1=notices all)"),
|
|
1466
|
-
anchoringBias: z.number().min(0).max(1).optional().describe("Influenced by first info seen (0=ignores, 1=anchors heavily)"),
|
|
1467
|
-
timeHorizon: z.number().min(0).max(1).optional().describe("Short vs long-term focus (0=immediate, 1=future-oriented)"),
|
|
1468
|
-
attributionStyle: z.number().min(0).max(1).optional().describe("Blames self vs external for failures (0=external, 1=internal)"),
|
|
1469
|
-
metacognitivePlanning: z.number().min(0).max(1).optional().describe("Plans approach before acting (0=impulsive, 1=strategic)"),
|
|
1470
|
-
proceduralFluency: z.number().min(0).max(1).optional().describe("Follows multi-step processes (0=struggles, 1=follows easily)"),
|
|
1471
|
-
transferLearning: z.number().min(0).max(1).optional().describe("Applies past learning to new contexts (0=compartmentalized, 1=transfers)"),
|
|
1472
|
-
authoritySensitivity: z.number().min(0).max(1).optional().describe("Responds to authority cues (0=ignores, 1=defers to authority)"),
|
|
1473
|
-
emotionalContagion: z.number().min(0).max(1).optional().describe("Affected by page emotional tone (0=immune, 1=absorbs mood)"),
|
|
1474
|
-
fearOfMissingOut: z.number().min(0).max(1).optional().describe("Responds to scarcity/urgency (0=immune, 1=strongly affected)"),
|
|
1475
|
-
socialProofSensitivity: z.number().min(0).max(1).optional().describe("Influenced by reviews/testimonials (0=ignores, 1=strongly influenced)"),
|
|
1476
|
-
// POLARITY CORRECTED 2026-08-01. This read "0=flexible, 1=rigid",
|
|
1477
|
-
// which is backwards from every other authority on the field: the
|
|
1478
|
-
// questionnaire runs 0 "Rigid Mental Models" to 1 "Extremely Flexible",
|
|
1479
|
-
// the glossary's high end is "Highly adaptive", and its typicalScores
|
|
1480
|
-
// put power-user high and elderly-user low. A caller trusting this
|
|
1481
|
-
// description would send 1.0 meaning rigid and get the most flexible
|
|
1482
|
-
// persona the scale can express, with nothing in the output looking
|
|
1483
|
-
// wrong -- exactly the failure the field's rename note warns about.
|
|
1484
|
-
mentalModelFlexibility: z.number().min(0).max(1).optional().describe("Mental-model adaptability. 0 = rigid, cannot adapt when conventions change; 1 = instantly adapts. Named 'Rigidity' for backwards compatibility; the SCALE runs toward flexibility."),
|
|
1485
|
-
}).optional().describe("Override specific cognitive traits (25 available)"),
|
|
1486
|
-
}, async ({ persona: personaName, goal, startUrl, customTraits }) => {
|
|
1487
|
-
// Get or create persona
|
|
1488
|
-
// v16.14.1: Use getAnyPersona to find personas in ALL registries
|
|
1489
|
-
const existingPersona = getAnyPersona(personaName);
|
|
1490
|
-
let personaObj;
|
|
1491
|
-
// v17.0.0: Check for agent personas - cognitive journeys don't support them yet
|
|
1492
|
-
if (existingPersona && isAgentPersonaObject(existingPersona)) {
|
|
1493
|
-
return {
|
|
1494
|
-
isError: true,
|
|
1495
|
-
content: [{
|
|
1496
|
-
type: "text",
|
|
1497
|
-
text: JSON.stringify({
|
|
1498
|
-
error: "Agent personas not supported for cognitive journeys",
|
|
1499
|
-
message: `"${personaName}" is an AI agent persona. Cognitive journeys simulate human behavior and require human personas. Use a human persona instead, or use agent-ready-audit for AI agent testing.`,
|
|
1500
|
-
suggestedPersonas: ["first-timer", "power-user", "mobile-user"],
|
|
1501
|
-
}, null, 2),
|
|
1502
|
-
}],
|
|
1503
|
-
};
|
|
1504
|
-
}
|
|
1505
|
-
if (!existingPersona) {
|
|
1506
|
-
// Create from description
|
|
1507
|
-
personaObj = createCognitivePersona(personaName, personaName, customTraits || {});
|
|
1508
|
-
}
|
|
1509
|
-
else if (customTraits) {
|
|
1510
|
-
// v16.11.0: full default trait set (was only 7, causing trait dropout).
|
|
1511
|
-
// Said 25 until 2026-08-01; the model has 26 and has for some time.
|
|
1512
|
-
const defaultTraits = {
|
|
1513
|
-
// Core 7 traits
|
|
1514
|
-
patience: 0.5,
|
|
1515
|
-
riskTolerance: 0.5,
|
|
1516
|
-
comprehension: 0.5,
|
|
1517
|
-
persistence: 0.5,
|
|
1518
|
-
curiosity: 0.5,
|
|
1519
|
-
workingMemory: 0.5,
|
|
1520
|
-
readingTendency: 0.5,
|
|
1521
|
-
// Tier 1: Core (5 more)
|
|
1522
|
-
resilience: 0.5,
|
|
1523
|
-
selfEfficacy: 0.5,
|
|
1524
|
-
satisficing: 0.5,
|
|
1525
|
-
trustCalibration: 0.5,
|
|
1526
|
-
interruptRecovery: 0.5,
|
|
1527
|
-
// Tier 2-6: Extended (13 more)
|
|
1528
|
-
informationForaging: 0.5,
|
|
1529
|
-
changeBlindness: 0.3,
|
|
1530
|
-
anchoringBias: 0.5,
|
|
1531
|
-
timeHorizon: 0.5,
|
|
1532
|
-
attributionStyle: 0.5,
|
|
1533
|
-
metacognitivePlanning: 0.5,
|
|
1534
|
-
proceduralFluency: 0.5,
|
|
1535
|
-
transferLearning: 0.5,
|
|
1536
|
-
authoritySensitivity: 0.5,
|
|
1537
|
-
emotionalContagion: 0.5,
|
|
1538
|
-
fearOfMissingOut: 0.5,
|
|
1539
|
-
socialProofSensitivity: 0.5,
|
|
1540
|
-
mentalModelFlexibility: 0.5,
|
|
1541
|
-
};
|
|
1542
|
-
personaObj = {
|
|
1543
|
-
...existingPersona,
|
|
1544
|
-
cognitiveTraits: {
|
|
1545
|
-
...defaultTraits,
|
|
1546
|
-
...(existingPersona.cognitiveTraits || {}),
|
|
1547
|
-
...customTraits,
|
|
1548
|
-
},
|
|
1549
|
-
};
|
|
1550
|
-
}
|
|
1551
|
-
else {
|
|
1552
|
-
personaObj = existingPersona;
|
|
1553
|
-
}
|
|
1554
|
-
// Get cognitive profile
|
|
1555
|
-
const profile = getCognitiveProfile(personaObj);
|
|
1556
|
-
// Initial cognitive state
|
|
1557
|
-
const initialState = {
|
|
1558
|
-
patienceRemaining: 1.0,
|
|
1559
|
-
confusionLevel: 0.0,
|
|
1560
|
-
frustrationLevel: 0.0,
|
|
1561
|
-
goalProgress: 0.0,
|
|
1562
|
-
confidenceLevel: 0.5,
|
|
1563
|
-
currentMood: "neutral",
|
|
1564
|
-
memory: {
|
|
1565
|
-
pagesVisited: [startUrl],
|
|
1566
|
-
actionsAttempted: [],
|
|
1567
|
-
errorsEncountered: [],
|
|
1568
|
-
backtrackCount: 0,
|
|
1569
|
-
},
|
|
1570
|
-
timeElapsed: 0,
|
|
1571
|
-
stepCount: 0,
|
|
1572
|
-
};
|
|
1573
|
-
// Abandonment thresholds (adjusted by persona traits)
|
|
1574
|
-
const traits = profile.traits;
|
|
1575
|
-
const thresholds = {
|
|
1576
|
-
patienceMin: 0.1,
|
|
1577
|
-
confusionMax: traits.comprehension < 0.4 ? 0.6 : 0.8, // Lower comprehension = lower tolerance
|
|
1578
|
-
frustrationMax: traits.patience < 0.3 ? 0.7 : 0.85, // Impatient = lower tolerance
|
|
1579
|
-
maxStepsWithoutProgress: traits.persistence > 0.7 ? 15 : 10,
|
|
1580
|
-
loopDetectionThreshold: 3,
|
|
1581
|
-
timeLimit: traits.patience > 0.7 ? 180 : (traits.patience < 0.3 ? 60 : 120),
|
|
1582
|
-
};
|
|
1583
|
-
// Navigate to start URL
|
|
1584
|
-
const b = await getBrowser();
|
|
1585
|
-
await b.navigate(startUrl);
|
|
1586
|
-
// v16.12.0: Include persona values for influence pattern analysis
|
|
1587
|
-
const personaValues = resolveValuesForPersona(personaObj.name);
|
|
1588
|
-
const influencePatterns = personaValues
|
|
1589
|
-
? rankInfluencePatternsForProfile(personaValues).slice(0, 5) // Top 5 most effective patterns
|
|
1590
|
-
: undefined;
|
|
1591
|
-
return {
|
|
1592
|
-
content: [
|
|
1593
|
-
{
|
|
1594
|
-
type: "text",
|
|
1595
|
-
text: JSON.stringify({
|
|
1596
|
-
persona: {
|
|
1597
|
-
name: personaObj.name,
|
|
1598
|
-
description: personaObj.description,
|
|
1599
|
-
demographics: personaObj.demographics,
|
|
1600
|
-
values: personaValues ? {
|
|
1601
|
-
schwartz: {
|
|
1602
|
-
selfDirection: personaValues.selfDirection,
|
|
1603
|
-
stimulation: personaValues.stimulation,
|
|
1604
|
-
hedonism: personaValues.hedonism,
|
|
1605
|
-
achievement: personaValues.achievement,
|
|
1606
|
-
power: personaValues.power,
|
|
1607
|
-
security: personaValues.security,
|
|
1608
|
-
conformity: personaValues.conformity,
|
|
1609
|
-
tradition: personaValues.tradition,
|
|
1610
|
-
benevolence: personaValues.benevolence,
|
|
1611
|
-
universalism: personaValues.universalism,
|
|
1612
|
-
},
|
|
1613
|
-
higherOrder: {
|
|
1614
|
-
openness: personaValues.openness,
|
|
1615
|
-
selfEnhancement: personaValues.selfEnhancement,
|
|
1616
|
-
conservation: personaValues.conservation,
|
|
1617
|
-
selfTranscendence: personaValues.selfTranscendence,
|
|
1618
|
-
},
|
|
1619
|
-
sdt: {
|
|
1620
|
-
autonomyNeed: personaValues.autonomyNeed,
|
|
1621
|
-
competenceNeed: personaValues.competenceNeed,
|
|
1622
|
-
relatednessNeed: personaValues.relatednessNeed,
|
|
1623
|
-
},
|
|
1624
|
-
maslowLevel: personaValues.maslowLevel,
|
|
1625
|
-
} : undefined,
|
|
1626
|
-
influenceSusceptibility: influencePatterns?.map(ip => ({
|
|
1627
|
-
pattern: ip.pattern.name,
|
|
1628
|
-
susceptibility: ip.susceptibility,
|
|
1629
|
-
})),
|
|
1630
|
-
},
|
|
1631
|
-
cognitiveProfile: profile,
|
|
1632
|
-
initialState,
|
|
1633
|
-
abandonmentThresholds: thresholds,
|
|
1634
|
-
goal,
|
|
1635
|
-
startUrl,
|
|
1636
|
-
instructions: `
|
|
1637
|
-
COGNITIVE JOURNEY SIMULATION INSTRUCTIONS:
|
|
1638
|
-
|
|
1639
|
-
You are now simulating a "${personaObj.name}" user with these cognitive traits:
|
|
1640
|
-
- Patience: ${profile.traits.patience.toFixed(2)} ${profile.traits.patience < 0.3 ? "(impatient - will give up quickly)" : profile.traits.patience > 0.7 ? "(patient - will persist)" : "(moderate)"}
|
|
1641
|
-
- Risk Tolerance: ${profile.traits.riskTolerance.toFixed(2)} ${profile.traits.riskTolerance < 0.3 ? "(cautious - hesitates)" : profile.traits.riskTolerance > 0.7 ? "(bold - clicks freely)" : "(moderate)"}
|
|
1642
|
-
- Comprehension: ${profile.traits.comprehension.toFixed(2)} ${profile.traits.comprehension < 0.3 ? "(struggles with UI)" : profile.traits.comprehension > 0.7 ? "(expert at UI patterns)" : "(moderate)"}
|
|
1643
|
-
- Reading Tendency: ${profile.traits.readingTendency.toFixed(2)} ${profile.traits.readingTendency < 0.3 ? "(scans only)" : profile.traits.readingTendency > 0.7 ? "(reads everything)" : "(selective reader)"}
|
|
1644
|
-
|
|
1645
|
-
Attention Pattern: ${profile.attentionPattern}
|
|
1646
|
-
Decision Style: ${profile.decisionStyle}
|
|
1647
|
-
|
|
1648
|
-
GOAL: "${goal}"
|
|
1649
|
-
|
|
1650
|
-
SIMULATION LOOP:
|
|
1651
|
-
1. PERCEIVE - Use screenshot/snapshot to see the page. Filter by attention pattern.
|
|
1652
|
-
2. COMPREHEND - Interpret elements as this persona would (lower comprehension = more confusion)
|
|
1653
|
-
3. DECIDE - Choose action based on traits. Generate inner monologue.
|
|
1654
|
-
4. EXECUTE - Use click/fill/navigate tools.
|
|
1655
|
-
5. EVALUATE - Update cognitive state after each action:
|
|
1656
|
-
- patienceRemaining -= 0.02 + (frustrationLevel × 0.05)
|
|
1657
|
-
- confusionLevel changes based on UI clarity
|
|
1658
|
-
- frustrationLevel increases on failures
|
|
1659
|
-
6. CHECK ABANDONMENT - If thresholds exceeded, end journey with appropriate message.
|
|
1660
|
-
7. LOOP - Return to PERCEIVE until goal achieved or abandoned.
|
|
1661
|
-
|
|
1662
|
-
ABANDONMENT TRIGGERS:
|
|
1663
|
-
- Patience < ${thresholds.patienceMin}: "This is taking too long. I give up."
|
|
1664
|
-
- Confusion > ${thresholds.confusionMax} for 30s: "I have no idea what to do."
|
|
1665
|
-
- Frustration > ${thresholds.frustrationMax}: "This is so frustrating!"
|
|
1666
|
-
- No progress after ${thresholds.maxStepsWithoutProgress} steps: "I'm not getting anywhere."
|
|
1667
|
-
- Same page ${thresholds.loopDetectionThreshold}x: "I keep ending up here."
|
|
1668
|
-
- Time > ${thresholds.timeLimit}s: "I've spent too long on this."
|
|
1669
|
-
|
|
1670
|
-
INNER MONOLOGUE EXAMPLES (${personaObj.name}):
|
|
1671
|
-
${profile.traits.patience < 0.3 ? '- "Come ON. Why is this taking so long?"' : '- "Let me take my time to figure this out..."'}
|
|
1672
|
-
${profile.traits.riskTolerance < 0.3 ? '- "I don\'t know what this button does. What if I click the wrong thing?"' : '- "This looks relevant, let me click it."'}
|
|
1673
|
-
${profile.traits.comprehension < 0.4 ? '- "What does this mean? I don\'t understand these icons."' : '- "Ah, I see - that\'s the settings menu."'}
|
|
1674
|
-
|
|
1675
|
-
Begin the simulation now. Narrate your thoughts as this persona.
|
|
1676
|
-
`,
|
|
1677
|
-
}, null, 2),
|
|
1678
|
-
},
|
|
1679
|
-
],
|
|
1680
|
-
};
|
|
1681
|
-
});
|
|
1682
|
-
server.tool("cognitive_journey_update_state", "Update the cognitive state during a journey simulation. Call this after each action to track mental state.", {
|
|
1683
|
-
currentState: z.object({
|
|
1684
|
-
patienceRemaining: z.number().describe("Remaining patience (0-1, depletes over time)"),
|
|
1685
|
-
confusionLevel: z.number().describe("Current confusion (0-1, high triggers abandonment)"),
|
|
1686
|
-
frustrationLevel: z.number().describe("Current frustration (0-1, high triggers abandonment)"),
|
|
1687
|
-
goalProgress: z.number().describe("Progress toward goal (0-1)"),
|
|
1688
|
-
confidenceLevel: z.number().describe("Self-confidence in completing task (0-1)"),
|
|
1689
|
-
currentMood: z.enum(["neutral", "hopeful", "confused", "frustrated", "defeated", "relieved"]).describe("Current emotional state"),
|
|
1690
|
-
stepCount: z.number().describe("Number of actions taken"),
|
|
1691
|
-
timeElapsed: z.number().describe("Seconds elapsed since journey start"),
|
|
1692
|
-
}).describe("Current cognitive state"),
|
|
1693
|
-
actionResult: z.object({
|
|
1694
|
-
success: z.boolean().describe("Whether the last action succeeded"),
|
|
1695
|
-
wasConfusing: z.boolean().optional().describe("Whether the UI was confusing"),
|
|
1696
|
-
progressMade: z.boolean().optional().describe("Whether progress was made toward goal"),
|
|
1697
|
-
wentBack: z.boolean().optional().describe("Whether user went back/undid action"),
|
|
1698
|
-
}).describe("Result of the last action"),
|
|
1699
|
-
personaTraits: z.object({
|
|
1700
|
-
patience: z.number().describe("Base patience trait (0-1)"),
|
|
1701
|
-
riskTolerance: z.number().describe("Willingness to try new things (0-1)"),
|
|
1702
|
-
comprehension: z.number().describe("UI understanding ability (0-1)"),
|
|
1703
|
-
persistence: z.number().describe("Tendency to retry same approach (0-1)"),
|
|
1704
|
-
}).describe("Persona traits affecting state changes"),
|
|
1705
|
-
}, async ({ currentState, actionResult, personaTraits }) => {
|
|
1706
|
-
// Calculate new state based on action result
|
|
1707
|
-
let newPatienceRemaining = currentState.patienceRemaining - 0.02;
|
|
1708
|
-
let newConfusionLevel = currentState.confusionLevel;
|
|
1709
|
-
let newFrustrationLevel = currentState.frustrationLevel;
|
|
1710
|
-
let newConfidenceLevel = currentState.confidenceLevel;
|
|
1711
|
-
let newMood = currentState.currentMood;
|
|
1712
|
-
// Apply frustration decay on patience
|
|
1713
|
-
newPatienceRemaining -= currentState.frustrationLevel * 0.05;
|
|
1714
|
-
if (actionResult.success) {
|
|
1715
|
-
// Success reduces confusion and frustration
|
|
1716
|
-
newConfusionLevel = Math.max(0, newConfusionLevel - 0.1);
|
|
1717
|
-
newFrustrationLevel = Math.max(0, newFrustrationLevel - 0.05);
|
|
1718
|
-
if (actionResult.progressMade) {
|
|
1719
|
-
newConfidenceLevel = Math.min(1, newConfidenceLevel + 0.1);
|
|
1720
|
-
if (newMood === "confused" || newMood === "frustrated") {
|
|
1721
|
-
newMood = "hopeful";
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
else {
|
|
1726
|
-
// Failure increases frustration
|
|
1727
|
-
newFrustrationLevel = Math.min(1, newFrustrationLevel + 0.2);
|
|
1728
|
-
if (newFrustrationLevel > 0.7) {
|
|
1729
|
-
newMood = "frustrated";
|
|
1730
|
-
}
|
|
1731
|
-
if (newFrustrationLevel > 0.8 && personaTraits.persistence < 0.5) {
|
|
1732
|
-
newMood = "defeated";
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
if (actionResult.wasConfusing) {
|
|
1736
|
-
// Confusion builds based on comprehension
|
|
1737
|
-
newConfusionLevel = Math.min(1, newConfusionLevel + (1 - personaTraits.comprehension) * 0.15);
|
|
1738
|
-
if (newConfusionLevel > 0.5 && newMood !== "frustrated") {
|
|
1739
|
-
newMood = "confused";
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
if (actionResult.wentBack) {
|
|
1743
|
-
newConfidenceLevel = Math.max(0, newConfidenceLevel - 0.15);
|
|
1744
|
-
}
|
|
1745
|
-
const newState = {
|
|
1746
|
-
patienceRemaining: Math.max(0, newPatienceRemaining),
|
|
1747
|
-
confusionLevel: newConfusionLevel,
|
|
1748
|
-
frustrationLevel: newFrustrationLevel,
|
|
1749
|
-
confidenceLevel: newConfidenceLevel,
|
|
1750
|
-
currentMood: newMood,
|
|
1751
|
-
stepCount: currentState.stepCount + 1,
|
|
1752
|
-
timeElapsed: currentState.timeElapsed + 2, // Estimate 2s per step
|
|
1753
|
-
};
|
|
1754
|
-
// Check abandonment conditions
|
|
1755
|
-
let shouldAbandon = false;
|
|
1756
|
-
let abandonmentReason;
|
|
1757
|
-
let abandonmentMessage;
|
|
1758
|
-
if (newState.patienceRemaining < 0.1) {
|
|
1759
|
-
shouldAbandon = true;
|
|
1760
|
-
abandonmentReason = "patience";
|
|
1761
|
-
abandonmentMessage = "This is taking too long. I give up.";
|
|
1762
|
-
}
|
|
1763
|
-
else if (newState.frustrationLevel > 0.85) {
|
|
1764
|
-
shouldAbandon = true;
|
|
1765
|
-
abandonmentReason = "frustration";
|
|
1766
|
-
abandonmentMessage = "This is so frustrating! I'm done.";
|
|
1767
|
-
}
|
|
1768
|
-
else if (newState.confusionLevel > 0.8 && currentState.confusionLevel > 0.8) {
|
|
1769
|
-
shouldAbandon = true;
|
|
1770
|
-
abandonmentReason = "confusion";
|
|
1771
|
-
abandonmentMessage = "I have no idea what I'm supposed to do here.";
|
|
1772
|
-
}
|
|
1773
|
-
return {
|
|
1774
|
-
content: [
|
|
1775
|
-
{
|
|
1776
|
-
type: "text",
|
|
1777
|
-
text: JSON.stringify({
|
|
1778
|
-
newState,
|
|
1779
|
-
shouldAbandon,
|
|
1780
|
-
abandonmentReason,
|
|
1781
|
-
abandonmentMessage,
|
|
1782
|
-
stateChange: {
|
|
1783
|
-
patienceDelta: newState.patienceRemaining - currentState.patienceRemaining,
|
|
1784
|
-
confusionDelta: newState.confusionLevel - currentState.confusionLevel,
|
|
1785
|
-
frustrationDelta: newState.frustrationLevel - currentState.frustrationLevel,
|
|
1786
|
-
},
|
|
1787
|
-
}, null, 2),
|
|
1788
|
-
},
|
|
1789
|
-
],
|
|
1790
|
-
};
|
|
1791
|
-
});
|
|
1792
|
-
server.tool("list_cognitive_personas", "List all available personas with their cognitive traits (includes accessibility and emotional personas)", {}, async () => {
|
|
1793
|
-
// v16.11.0: Include all persona types - BUILTIN + ACCESSIBILITY + EMOTIONAL
|
|
1794
|
-
const builtinNames = listPersonas();
|
|
1795
|
-
const accessibilityNames = listAccessibilityPersonas();
|
|
1796
|
-
// Built-in personas (power-user, first-timer, etc.)
|
|
1797
|
-
const builtinPersonas = builtinNames.map(name => {
|
|
1798
|
-
const p = getPersona(name);
|
|
1799
|
-
if (!p)
|
|
1800
|
-
return null;
|
|
1801
|
-
const profile = getCognitiveProfile(p);
|
|
1802
|
-
// v16.12.0: Include Schwartz values for each persona
|
|
1803
|
-
const values = resolveValuesForPersona(p.name);
|
|
1804
|
-
return {
|
|
1805
|
-
name: p.name,
|
|
1806
|
-
description: p.description,
|
|
1807
|
-
category: "builtin",
|
|
1808
|
-
demographics: p.demographics,
|
|
1809
|
-
cognitiveTraits: profile.traits,
|
|
1810
|
-
attentionPattern: profile.attentionPattern,
|
|
1811
|
-
decisionStyle: profile.decisionStyle,
|
|
1812
|
-
values: values ? {
|
|
1813
|
-
schwartz: {
|
|
1814
|
-
selfDirection: values.selfDirection,
|
|
1815
|
-
stimulation: values.stimulation,
|
|
1816
|
-
hedonism: values.hedonism,
|
|
1817
|
-
achievement: values.achievement,
|
|
1818
|
-
power: values.power,
|
|
1819
|
-
security: values.security,
|
|
1820
|
-
conformity: values.conformity,
|
|
1821
|
-
tradition: values.tradition,
|
|
1822
|
-
benevolence: values.benevolence,
|
|
1823
|
-
universalism: values.universalism,
|
|
1824
|
-
},
|
|
1825
|
-
higherOrder: {
|
|
1826
|
-
openness: values.openness,
|
|
1827
|
-
selfEnhancement: values.selfEnhancement,
|
|
1828
|
-
conservation: values.conservation,
|
|
1829
|
-
selfTranscendence: values.selfTranscendence,
|
|
1830
|
-
},
|
|
1831
|
-
sdt: {
|
|
1832
|
-
autonomyNeed: values.autonomyNeed,
|
|
1833
|
-
competenceNeed: values.competenceNeed,
|
|
1834
|
-
relatednessNeed: values.relatednessNeed,
|
|
1835
|
-
},
|
|
1836
|
-
maslowLevel: values.maslowLevel,
|
|
1837
|
-
} : undefined,
|
|
1838
|
-
};
|
|
1839
|
-
}).filter(Boolean);
|
|
1840
|
-
// Accessibility personas (motor-tremor, low-vision, adhd, etc.)
|
|
1841
|
-
const accessibilityPersonas = accessibilityNames.map(name => {
|
|
1842
|
-
const p = getAccessibilityPersona(name);
|
|
1843
|
-
if (!p)
|
|
1844
|
-
return null;
|
|
1845
|
-
// Shared with the remote roster (personas.ts); a declared label wins
|
|
1846
|
-
// over inference. This copy and that one were identical inference
|
|
1847
|
-
// blocks, so a fix to one could only ever reach half the users.
|
|
1848
|
-
// (2026-10-07)
|
|
1849
|
-
const { disabilityType, barrierTypes } = describeAccessibilityPersona(p);
|
|
1850
|
-
// v16.12.0: Include Schwartz values for accessibility personas
|
|
1851
|
-
const values = resolveValuesForPersona(p.name);
|
|
1852
|
-
return {
|
|
1853
|
-
name: p.name,
|
|
1854
|
-
description: p.description,
|
|
1855
|
-
category: "accessibility",
|
|
1856
|
-
disabilityType,
|
|
1857
|
-
demographics: p.demographics,
|
|
1858
|
-
cognitiveTraits: p.cognitiveTraits || {},
|
|
1859
|
-
barrierTypes,
|
|
1860
|
-
values: values ? {
|
|
1861
|
-
schwartz: {
|
|
1862
|
-
selfDirection: values.selfDirection,
|
|
1863
|
-
stimulation: values.stimulation,
|
|
1864
|
-
hedonism: values.hedonism,
|
|
1865
|
-
achievement: values.achievement,
|
|
1866
|
-
power: values.power,
|
|
1867
|
-
security: values.security,
|
|
1868
|
-
conformity: values.conformity,
|
|
1869
|
-
tradition: values.tradition,
|
|
1870
|
-
benevolence: values.benevolence,
|
|
1871
|
-
universalism: values.universalism,
|
|
1872
|
-
},
|
|
1873
|
-
higherOrder: {
|
|
1874
|
-
openness: values.openness,
|
|
1875
|
-
selfEnhancement: values.selfEnhancement,
|
|
1876
|
-
conservation: values.conservation,
|
|
1877
|
-
selfTranscendence: values.selfTranscendence,
|
|
1878
|
-
},
|
|
1879
|
-
sdt: {
|
|
1880
|
-
autonomyNeed: values.autonomyNeed,
|
|
1881
|
-
competenceNeed: values.competenceNeed,
|
|
1882
|
-
relatednessNeed: values.relatednessNeed,
|
|
1883
|
-
},
|
|
1884
|
-
maslowLevel: values.maslowLevel,
|
|
1885
|
-
} : undefined,
|
|
1886
|
-
};
|
|
1887
|
-
}).filter(Boolean);
|
|
1888
|
-
const allPersonas = [...builtinPersonas, ...accessibilityPersonas];
|
|
1889
|
-
return {
|
|
1890
|
-
content: [
|
|
1891
|
-
{
|
|
1892
|
-
type: "text",
|
|
1893
|
-
text: JSON.stringify({
|
|
1894
|
-
personas: allPersonas,
|
|
1895
|
-
count: allPersonas.length,
|
|
1896
|
-
categories: {
|
|
1897
|
-
builtin: builtinPersonas.length,
|
|
1898
|
-
accessibility: accessibilityPersonas.length,
|
|
1899
|
-
},
|
|
1900
|
-
}, null, 2),
|
|
1901
|
-
},
|
|
1902
|
-
],
|
|
1903
|
-
};
|
|
1904
|
-
});
|
|
1905
|
-
// =========================================================================
|
|
1906
|
-
// Persona Questionnaire Tools (v16.5.0)
|
|
1907
|
-
// Research-based persona generation via questionnaire
|
|
1908
|
-
// =========================================================================
|
|
1909
|
-
// =========================================================================
|
|
1910
|
-
// Values System Tools (v16.12.0)
|
|
1911
|
-
// Schwartz's 10 Universal Values, Self-Determination Theory, Maslow
|
|
1912
|
-
// =========================================================================
|
|
1913
|
-
// persona_values_lookup lives in mcp-tools/base/values-tools.ts and is
|
|
1914
|
-
// registered below via registerValuesTools. A second copy lived here and
|
|
1915
|
-
// answered the stdio server while the remote server answered from the
|
|
1916
|
-
// shared one: same tool name, two different payloads, and the newer fields
|
|
1917
|
-
// (netNudge, valuesSource, the maslow basis) existed in only one of them.
|
|
1918
|
-
// The divergence was invisible because each server was self-consistent.
|
|
1919
|
-
// Same removal as the legacy empathy_audit above. (2026-08-01)
|
|
1920
|
-
// =========================================================================
|
|
1921
|
-
// Persona Comparison Session Bridge (API-free via Claude orchestration)
|
|
1922
|
-
// =========================================================================
|
|
1923
|
-
server.tool("compare_personas_record_result", "Record the journey result for a persona in the comparison session. Call this when a persona's journey is complete (success or abandonment).", {
|
|
1924
|
-
sessionId: z.string().describe("Session ID from compare_personas_init"),
|
|
1925
|
-
persona: z.string().describe("Persona name"),
|
|
1926
|
-
goalAchieved: z.boolean().describe("Whether the goal was accomplished"),
|
|
1927
|
-
abandonmentReason: z.enum(["patience", "confusion", "frustration", "no_progress", "loop", "timeout"]).optional().describe("Why the persona abandoned (if goalAchieved is false)"),
|
|
1928
|
-
finalState: z.object({
|
|
1929
|
-
patienceRemaining: z.number().describe("Final patience level (0-1)"),
|
|
1930
|
-
confusionLevel: z.number().describe("Final confusion level (0-1)"),
|
|
1931
|
-
frustrationLevel: z.number().describe("Final frustration level (0-1)"),
|
|
1932
|
-
stepCount: z.number().describe("Total number of actions taken"),
|
|
1933
|
-
timeElapsed: z.number().describe("Total seconds elapsed"),
|
|
1934
|
-
}).describe("Final cognitive state"),
|
|
1935
|
-
frictionPoints: z.array(z.object({
|
|
1936
|
-
type: z.string().describe("Type of friction (e.g., 'navigation', 'form', 'loading')"),
|
|
1937
|
-
description: z.string().describe("Description of the friction point"),
|
|
1938
|
-
})).optional().describe("Friction points encountered during journey"),
|
|
1939
|
-
}, async ({ sessionId, persona, goalAchieved, abandonmentReason, finalState, frictionPoints }) => {
|
|
1940
|
-
const session = comparisonSessions.get(sessionId);
|
|
1941
|
-
if (!session) {
|
|
1942
|
-
return {
|
|
1943
|
-
isError: true,
|
|
1944
|
-
content: [{ type: "text", text: JSON.stringify({ error: "Session not found", sessionId }) }],
|
|
1945
|
-
};
|
|
1946
|
-
}
|
|
1947
|
-
// Add result
|
|
1948
|
-
session.results.push({
|
|
1949
|
-
persona,
|
|
1950
|
-
goalAchieved,
|
|
1951
|
-
abandonmentReason,
|
|
1952
|
-
finalState: {
|
|
1953
|
-
...finalState,
|
|
1954
|
-
goalProgress: goalAchieved ? 1.0 : 0.5,
|
|
1955
|
-
confidenceLevel: goalAchieved ? 0.9 : 0.3,
|
|
1956
|
-
currentMood: goalAchieved ? "relieved" : "defeated",
|
|
1957
|
-
memory: {
|
|
1958
|
-
pagesVisited: [],
|
|
1959
|
-
actionsAttempted: [],
|
|
1960
|
-
errorsEncountered: [],
|
|
1961
|
-
backtrackCount: 0,
|
|
1962
|
-
},
|
|
1963
|
-
},
|
|
1964
|
-
stepCount: finalState.stepCount,
|
|
1965
|
-
timeElapsed: finalState.timeElapsed,
|
|
1966
|
-
frictionPoints: frictionPoints || [],
|
|
1967
|
-
});
|
|
1968
|
-
const remaining = session.personas.length - session.results.length;
|
|
1969
|
-
return {
|
|
1970
|
-
content: [
|
|
1971
|
-
{
|
|
1972
|
-
type: "text",
|
|
1973
|
-
text: JSON.stringify({
|
|
1974
|
-
recorded: true,
|
|
1975
|
-
persona,
|
|
1976
|
-
goalAchieved,
|
|
1977
|
-
resultCount: session.results.length,
|
|
1978
|
-
totalPersonas: session.personas.length,
|
|
1979
|
-
remaining,
|
|
1980
|
-
nextStep: remaining > 0
|
|
1981
|
-
? `Run journey for ${remaining} more persona(s), then call compare_personas_summarize`
|
|
1982
|
-
: "All personas complete. Call compare_personas_summarize to get the comparison report.",
|
|
1983
|
-
}, null, 2),
|
|
1984
|
-
},
|
|
1985
|
-
],
|
|
1986
|
-
};
|
|
1987
|
-
});
|
|
1988
|
-
server.tool("compare_personas_summarize", "Generate the final comparison summary after all persona journeys are complete. Returns rankings, insights, and recommendations.", {
|
|
1989
|
-
sessionId: z.string().describe("Session ID from compare_personas_init"),
|
|
1990
|
-
}, async ({ sessionId }) => {
|
|
1991
|
-
const session = comparisonSessions.get(sessionId);
|
|
1992
|
-
if (!session) {
|
|
1993
|
-
return {
|
|
1994
|
-
isError: true,
|
|
1995
|
-
content: [{ type: "text", text: JSON.stringify({ error: "Session not found", sessionId }) }],
|
|
1996
|
-
};
|
|
1997
|
-
}
|
|
1998
|
-
if (session.results.length === 0) {
|
|
1999
|
-
return {
|
|
2000
|
-
isError: true,
|
|
2001
|
-
content: [{ type: "text", text: JSON.stringify({ error: "No results recorded yet", sessionId }) }],
|
|
2002
|
-
};
|
|
2003
|
-
}
|
|
2004
|
-
// Generate summary (deterministic aggregation)
|
|
2005
|
-
const successfulResults = session.results.filter(r => r.goalAchieved);
|
|
2006
|
-
const failedResults = session.results.filter(r => !r.goalAchieved);
|
|
2007
|
-
const sortedByTime = [...successfulResults].sort((a, b) => a.timeElapsed - b.timeElapsed);
|
|
2008
|
-
const sortedBySteps = [...successfulResults].sort((a, b) => a.stepCount - b.stepCount);
|
|
2009
|
-
const sortedByFriction = [...session.results].sort((a, b) => b.frictionPoints.length - a.frictionPoints.length);
|
|
2010
|
-
// Collect all friction points
|
|
2011
|
-
const allFrictionPoints = session.results.flatMap(r => r.frictionPoints.map(fp => fp.type));
|
|
2012
|
-
const frictionCounts = allFrictionPoints.reduce((acc, fp) => {
|
|
2013
|
-
acc[fp] = (acc[fp] || 0) + 1;
|
|
2014
|
-
return acc;
|
|
2015
|
-
}, {});
|
|
2016
|
-
const commonFriction = Object.entries(frictionCounts)
|
|
2017
|
-
.filter(([_, count]) => count > 1)
|
|
2018
|
-
.sort((a, b) => b[1] - a[1])
|
|
2019
|
-
.slice(0, 5)
|
|
2020
|
-
.map(([fp]) => fp);
|
|
2021
|
-
// Generate recommendations
|
|
2022
|
-
const recommendations = [];
|
|
2023
|
-
// Abandonment analysis
|
|
2024
|
-
const abandonedByPatience = failedResults.filter(r => r.abandonmentReason === "patience");
|
|
2025
|
-
const abandonedByFrustration = failedResults.filter(r => r.abandonmentReason === "frustration");
|
|
2026
|
-
const abandonedByConfusion = failedResults.filter(r => r.abandonmentReason === "confusion");
|
|
2027
|
-
if (abandonedByPatience.length > 0) {
|
|
2028
|
-
recommendations.push(`${abandonedByPatience.length} persona(s) abandoned due to PATIENCE exhaustion: ${abandonedByPatience.map(r => r.persona).join(", ")} - consider shorter flows`);
|
|
2029
|
-
}
|
|
2030
|
-
if (abandonedByFrustration.length > 0) {
|
|
2031
|
-
recommendations.push(`${abandonedByFrustration.length} persona(s) abandoned due to FRUSTRATION: ${abandonedByFrustration.map(r => r.persona).join(", ")} - review error messages and feedback`);
|
|
2032
|
-
}
|
|
2033
|
-
if (abandonedByConfusion.length > 0) {
|
|
2034
|
-
recommendations.push(`${abandonedByConfusion.length} persona(s) abandoned due to CONFUSION: ${abandonedByConfusion.map(r => r.persona).join(", ")} - improve UI clarity and labeling`);
|
|
2035
|
-
}
|
|
2036
|
-
if (sortedByFriction[0]?.frictionPoints.length > 0) {
|
|
2037
|
-
recommendations.push(`"${sortedByFriction[0].persona}" experienced the most friction (${sortedByFriction[0].frictionPoints.length} points)`);
|
|
91
|
+
if (session.createdAt < oneHourAgo) {
|
|
92
|
+
empathyAuditSessions.delete(id);
|
|
2038
93
|
}
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Helper: Get barrier hints based on persona traits
|
|
97
|
+
function getBarrierHintsForPersona(persona) {
|
|
98
|
+
const hints = [];
|
|
99
|
+
const traits = persona.accessibilityTraits;
|
|
100
|
+
if (traits?.motorControl !== undefined && traits.motorControl < 0.5) {
|
|
101
|
+
hints.push("Watch for small click targets (<44px), precise hover requirements, drag-and-drop interactions");
|
|
102
|
+
}
|
|
103
|
+
if (traits?.tremor) {
|
|
104
|
+
hints.push("Test for accidental double-clicks, cursor jitter tolerance, need for 'undo' options");
|
|
105
|
+
}
|
|
106
|
+
if (traits?.visionLevel !== undefined && traits.visionLevel < 0.5) {
|
|
107
|
+
hints.push("Check contrast ratios, text scaling support, zoom behavior at 200-300%");
|
|
108
|
+
}
|
|
109
|
+
if (traits?.colorBlindness) {
|
|
110
|
+
hints.push(`Check for color-only information (${traits.colorBlindness} colorblindness), ensure status indicators have non-color cues`);
|
|
111
|
+
}
|
|
112
|
+
if (traits?.processingSpeed !== undefined && traits.processingSpeed < 0.5) {
|
|
113
|
+
hints.push("Watch for time limits, auto-advancing content, complex multi-step processes");
|
|
114
|
+
}
|
|
115
|
+
if (traits?.attentionSpan !== undefined && traits.attentionSpan < 0.5) {
|
|
116
|
+
hints.push("Note distracting animations, long forms, lack of progress indicators");
|
|
117
|
+
}
|
|
118
|
+
// Check for hearing-related disability
|
|
119
|
+
const disabilityType = persona.disabilityType || "";
|
|
120
|
+
const personaName = persona.name || "";
|
|
121
|
+
if (disabilityType.includes("hearing") || disabilityType.includes("deaf") || personaName.includes("deaf") || personaName.includes("hearing")) {
|
|
122
|
+
hints.push("Check for audio-only content, video captions, visual alerts for audio notifications");
|
|
123
|
+
}
|
|
124
|
+
if (hints.length === 0) {
|
|
125
|
+
hints.push("Observe general usability and any unexpected difficulties");
|
|
126
|
+
}
|
|
127
|
+
return hints;
|
|
128
|
+
}
|
|
129
|
+
// Helper: Get remediation suggestion for barrier type
|
|
130
|
+
function getRemediationForBarrier(barrierType, element) {
|
|
131
|
+
const remediations = {
|
|
132
|
+
motor_precision: `Increase target size to at least 44x44px for "${element}". Add generous padding and spacing.`,
|
|
133
|
+
visual_clarity: `Improve contrast ratio to at least 4.5:1 for "${element}". Ensure text scales properly.`,
|
|
134
|
+
cognitive_load: `Simplify "${element}" - reduce options, add clear labels, provide inline help.`,
|
|
135
|
+
temporal: `Remove or extend time limits on "${element}". Allow users to pause/extend deadlines.`,
|
|
136
|
+
sensory: `Add text alternative for "${element}". Don't rely on color alone to convey information.`,
|
|
137
|
+
contrast: `Increase contrast ratio for "${element}" to at least 4.5:1 (3:1 for large text).`,
|
|
138
|
+
touch_target: `Increase touch target size for "${element}" to minimum 44x44px (WCAG 2.5.8).`,
|
|
139
|
+
timing: `Extend or remove timing constraints on "${element}". Provide pause/stop controls.`,
|
|
140
|
+
};
|
|
141
|
+
return remediations[barrierType] || `Review "${element}" for accessibility improvements.`;
|
|
142
|
+
}
|
|
143
|
+
// Helper: Derive disability type from persona traits
|
|
144
|
+
function getDisabilityTypeFromPersona(persona) {
|
|
145
|
+
const traits = persona.accessibilityTraits;
|
|
146
|
+
if (traits?.tremor)
|
|
147
|
+
return "Motor impairment (tremor)";
|
|
148
|
+
if (traits?.visionLevel !== undefined && traits.visionLevel < 0.5)
|
|
149
|
+
return "Low vision";
|
|
150
|
+
if (traits?.colorBlindness)
|
|
151
|
+
return `Color blindness (${traits.colorBlindness})`;
|
|
152
|
+
if (persona.cognitiveTraits?.workingMemory !== undefined && persona.cognitiveTraits.workingMemory < 0.5)
|
|
153
|
+
return "Cognitive (ADHD/Memory)";
|
|
154
|
+
if (traits?.processingSpeed !== undefined && traits.processingSpeed < 0.6)
|
|
155
|
+
return "Cognitive (Processing)";
|
|
156
|
+
// Fallback to name-based detection
|
|
157
|
+
if (persona.name.includes("deaf") || persona.name.includes("hearing"))
|
|
158
|
+
return "Hearing impairment";
|
|
159
|
+
if (persona.name.includes("motor"))
|
|
160
|
+
return "Motor impairment";
|
|
161
|
+
if (persona.name.includes("vision") || persona.name.includes("blind"))
|
|
162
|
+
return "Vision impairment";
|
|
163
|
+
if (persona.name.includes("cognitive") || persona.name.includes("adhd"))
|
|
164
|
+
return "Cognitive";
|
|
165
|
+
if (persona.name.includes("elderly"))
|
|
166
|
+
return "Age-related impairments";
|
|
167
|
+
if (persona.name.includes("dyslexic"))
|
|
168
|
+
return "Dyslexia";
|
|
169
|
+
return "General accessibility";
|
|
170
|
+
}
|
|
171
|
+
// Helper: Generate recommendations from empathy audit
|
|
172
|
+
function generateEmpathyRecommendations(session) {
|
|
173
|
+
const recommendations = [];
|
|
174
|
+
// Check success rate
|
|
175
|
+
const successRate = session.personaResults.filter(r => r.goalAchieved).length / session.personaResults.length;
|
|
176
|
+
if (successRate < 0.5) {
|
|
177
|
+
recommendations.push("CRITICAL: Less than 50% of disability personas could complete the goal. Fundamental accessibility improvements needed.");
|
|
178
|
+
}
|
|
179
|
+
else if (successRate < 0.8) {
|
|
180
|
+
recommendations.push("Several disability personas struggled to complete the goal. Review barriers by persona type.");
|
|
181
|
+
}
|
|
182
|
+
// Check for critical barriers
|
|
183
|
+
const criticalBarriers = session.barriers.filter(b => b.severity === "critical");
|
|
184
|
+
if (criticalBarriers.length > 0) {
|
|
185
|
+
recommendations.push(`${criticalBarriers.length} critical barriers found. Address these first as they prevent task completion.`);
|
|
186
|
+
}
|
|
187
|
+
// Check WCAG violations by level
|
|
188
|
+
const levelAViolations = Array.from(session.wcagViolations).filter(c => WCAG_CRITERIA[c]?.level === "A");
|
|
189
|
+
if (levelAViolations.length > 0) {
|
|
190
|
+
recommendations.push(`${levelAViolations.length} WCAG Level A violations (minimum compliance). These are legally required in most jurisdictions.`);
|
|
191
|
+
}
|
|
192
|
+
// Persona-specific recommendations
|
|
193
|
+
const worstPersona = session.personaResults.sort((a, b) => a.empathyScore - b.empathyScore)[0];
|
|
194
|
+
if (worstPersona && worstPersona.empathyScore < 50) {
|
|
195
|
+
recommendations.push(`"${worstPersona.persona}" (${worstPersona.disabilityType}) had the worst experience (score: ${worstPersona.empathyScore}). Prioritize improvements for this user group.`);
|
|
196
|
+
}
|
|
197
|
+
if (recommendations.length === 0) {
|
|
198
|
+
recommendations.push("Good accessibility foundation. Continue testing with real users with disabilities for deeper insights.");
|
|
199
|
+
}
|
|
200
|
+
return recommendations;
|
|
201
|
+
}
|
|
202
|
+
/** Name and description of every tool tools/list will serve, for security_audit. */
|
|
203
|
+
function listRegisteredTools(server) {
|
|
204
|
+
const registry = server._registeredTools ?? {};
|
|
205
|
+
return Object.entries(registry)
|
|
206
|
+
.filter(([, t]) => t?.enabled !== false)
|
|
207
|
+
.map(([name, t]) => ({ name, description: t.description ?? "" }));
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Register all CBrowser tools on an MCP server instance.
|
|
211
|
+
* Internal function - use createMcpServer() for the public API.
|
|
212
|
+
*/
|
|
213
|
+
async function registerCBrowserTools() {
|
|
214
|
+
// Auto-initialize all data directories on server start
|
|
215
|
+
ensureDirectories();
|
|
216
|
+
const server = new McpServer({
|
|
217
|
+
name: "cbrowser",
|
|
218
|
+
version: VERSION,
|
|
2082
219
|
});
|
|
220
|
+
// The shared tool set, the same registrar the hosted servers call. This file
|
|
221
|
+
// used to hand-write its own copies of ~45 tools and import a few shared
|
|
222
|
+
// families on top; every tool added to the shared path after the copy was
|
|
223
|
+
// made never reached `npx cbrowser mcp-server` (measured 2026-10-21: stdio 82
|
|
224
|
+
// tools, hosted 129, missing evaluate_script, hover, type_text, press_key,
|
|
225
|
+
// drag, upload_file, handle_dialog, manage_*, page_understand, attention_*,
|
|
226
|
+
// llms_txt_* and more), and the copies drifted (navigate here hardcoded
|
|
227
|
+
// `success: true` long after the shared one forwarded the browser's verdict).
|
|
228
|
+
//
|
|
229
|
+
// Tier null = self-hosted: no tier gate, no credits. Hosted path and URL
|
|
230
|
+
// refusals key on the hosted-mode flag, which only the remote server sets,
|
|
231
|
+
// and screenshots stay file paths because remote mode is never set here.
|
|
232
|
+
// registerAllPublicTools also attaches the security layer (audit log,
|
|
233
|
+
// description scan, zone checks); it wraps this server object in place, so
|
|
234
|
+
// the stdio-only tools registered below pass through it too.
|
|
235
|
+
registerAllPublicTools(server, { getBrowser }, null);
|
|
236
|
+
// The ui:// resources the canonical tools advertise via _meta.ui. Without
|
|
237
|
+
// these the stdio server answered "Method not found" to resources/list while
|
|
238
|
+
// its own tool pointed at ui://cbrowser/empathy, so a UI-capable host was
|
|
239
|
+
// told a widget existed and could not fetch it.
|
|
240
|
+
registerUiResources(server);
|
|
241
|
+
// Only stdio-only tools below: session bridges whose state lives in this
|
|
242
|
+
// process. Anything that exists on the shared path belongs there, not here.
|
|
243
|
+
// compare_personas_record_result and compare_personas_summarize were removed
|
|
244
|
+
// in 20.0.0: nothing created the session they looked up (the stdio
|
|
245
|
+
// compare_personas_init went 2026-08-01 and the shared one returns no
|
|
246
|
+
// sessionId), so both always answered "Session not found" (audit M, Alexa
|
|
247
|
+
// 2026-10-21).
|
|
2083
248
|
// =========================================================================
|
|
2084
249
|
// Empathy Audit Session Bridge (API-free via Claude orchestration)
|
|
2085
250
|
// =========================================================================
|
|
@@ -2367,257 +532,6 @@ Begin the simulation now. Narrate your thoughts as this persona.
|
|
|
2367
532
|
};
|
|
2368
533
|
});
|
|
2369
534
|
// =========================================================================
|
|
2370
|
-
// Performance Tools (v6.4.0+)
|
|
2371
|
-
// =========================================================================
|
|
2372
|
-
server.tool("perf_baseline", "Capture performance baseline for a URL", {
|
|
2373
|
-
url: z.string().url().describe("URL to capture baseline for"),
|
|
2374
|
-
name: z.string().describe("Name for the baseline"),
|
|
2375
|
-
runs: z.number().optional().default(3).describe("Number of runs to average"),
|
|
2376
|
-
}, async ({ url, name, runs }) => {
|
|
2377
|
-
const result = await capturePerformanceBaseline(url, { name, runs });
|
|
2378
|
-
// v16.11.0: Return all available metrics, not just core 4
|
|
2379
|
-
const m = result.metrics;
|
|
2380
|
-
return {
|
|
2381
|
-
content: [
|
|
2382
|
-
{
|
|
2383
|
-
type: "text",
|
|
2384
|
-
text: JSON.stringify({
|
|
2385
|
-
name: result.name,
|
|
2386
|
-
url: result.url,
|
|
2387
|
-
// Core Web Vitals
|
|
2388
|
-
coreWebVitals: {
|
|
2389
|
-
lcp: m.lcp,
|
|
2390
|
-
lcpRating: m.lcpRating,
|
|
2391
|
-
fid: m.fid,
|
|
2392
|
-
fidRating: m.fidRating,
|
|
2393
|
-
cls: m.cls,
|
|
2394
|
-
clsRating: m.clsRating,
|
|
2395
|
-
},
|
|
2396
|
-
// Additional timing metrics
|
|
2397
|
-
timingMetrics: {
|
|
2398
|
-
fcp: m.fcp,
|
|
2399
|
-
fcpRating: m.fcpRating,
|
|
2400
|
-
ttfb: m.ttfb,
|
|
2401
|
-
ttfbRating: m.ttfbRating,
|
|
2402
|
-
tti: m.tti,
|
|
2403
|
-
tbt: m.tbt,
|
|
2404
|
-
domContentLoaded: m.domContentLoaded,
|
|
2405
|
-
load: m.load,
|
|
2406
|
-
},
|
|
2407
|
-
// Resource metrics
|
|
2408
|
-
resourceMetrics: {
|
|
2409
|
-
resourceCount: m.resourceCount,
|
|
2410
|
-
transferSize: m.transferSize,
|
|
2411
|
-
},
|
|
2412
|
-
// Flat copy for backward compatibility
|
|
2413
|
-
metrics: {
|
|
2414
|
-
lcp: m.lcp,
|
|
2415
|
-
fcp: m.fcp,
|
|
2416
|
-
ttfb: m.ttfb,
|
|
2417
|
-
cls: m.cls,
|
|
2418
|
-
},
|
|
2419
|
-
}, null, 2),
|
|
2420
|
-
},
|
|
2421
|
-
],
|
|
2422
|
-
};
|
|
2423
|
-
});
|
|
2424
|
-
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 (perf envs, FCP 10%/50ms), normal (default, FCP 20%/100ms), ci (automated pipelines, FCP 25%/150ms), lenient (dev, FCP 30%/200ms).", {
|
|
2425
|
-
url: z.string().url().describe("URL to test"),
|
|
2426
|
-
baselineName: z.string().describe("Name of baseline to compare against"),
|
|
2427
|
-
sensitivity: z.enum(["strict", "normal", "ci", "lenient"]).optional().default("normal").describe("Sensitivity profile: strict (perf testing), normal (local dev), ci (automated pipelines), lenient (development)"),
|
|
2428
|
-
thresholdLcp: z.number().optional().describe("Override LCP threshold percentage"),
|
|
2429
|
-
}, async ({ url, baselineName, sensitivity, thresholdLcp }) => {
|
|
2430
|
-
const result = await detectPerformanceRegression(url, baselineName, {
|
|
2431
|
-
sensitivity,
|
|
2432
|
-
thresholds: thresholdLcp ? { lcp: thresholdLcp } : undefined,
|
|
2433
|
-
});
|
|
2434
|
-
return {
|
|
2435
|
-
content: [
|
|
2436
|
-
{
|
|
2437
|
-
type: "text",
|
|
2438
|
-
text: JSON.stringify({
|
|
2439
|
-
passed: result.passed,
|
|
2440
|
-
sensitivity: result.sensitivity,
|
|
2441
|
-
notes: result.notes,
|
|
2442
|
-
regressions: result.regressions,
|
|
2443
|
-
currentMetrics: result.currentMetrics,
|
|
2444
|
-
baseline: result.baseline.name,
|
|
2445
|
-
}, null, 2),
|
|
2446
|
-
},
|
|
2447
|
-
],
|
|
2448
|
-
};
|
|
2449
|
-
});
|
|
2450
|
-
server.tool("list_baselines", "List all saved baselines (visual and performance)", {}, async () => {
|
|
2451
|
-
// visual_baseline stores smart baselines; list both kinds (v10 B36).
|
|
2452
|
-
const visualBaselines = [...(await listVisualBaselines()), ...listSmartBaselineSummaries()];
|
|
2453
|
-
const perfBaselines = await listPerformanceBaselines();
|
|
2454
|
-
return {
|
|
2455
|
-
content: [
|
|
2456
|
-
{
|
|
2457
|
-
type: "text",
|
|
2458
|
-
text: JSON.stringify({
|
|
2459
|
-
visual: visualBaselines,
|
|
2460
|
-
performance: perfBaselines,
|
|
2461
|
-
}, null, 2),
|
|
2462
|
-
},
|
|
2463
|
-
],
|
|
2464
|
-
};
|
|
2465
|
-
});
|
|
2466
|
-
// =========================================================================
|
|
2467
|
-
// Agent-Ready Audit, Competitive Benchmark, Accessibility Empathy (v8.0.0)
|
|
2468
|
-
// =========================================================================
|
|
2469
|
-
server.tool("agent_ready_audit", "Audit a website for AI-agent friendliness. Analyzes findability, stability, accessibility, and semantics. Returns score (0-100), grade (A-F), issues, and remediation recommendations.", {
|
|
2470
|
-
url: z.string().url().describe("URL to audit"),
|
|
2471
|
-
limit: z.number().optional().default(5).describe("How many issues (worst first) and recommendations (priority order) to return. Totals and severity counts always cover ALL findings."),
|
|
2472
|
-
offset: z.number().optional().default(0).describe("Skip this many issues and recommendations before returning, for paging through a large result."),
|
|
2473
|
-
}, async ({ url, limit, offset }) => {
|
|
2474
|
-
const result = await runAgentReadyAudit(url, { headless: true });
|
|
2475
|
-
return {
|
|
2476
|
-
content: [
|
|
2477
|
-
{
|
|
2478
|
-
type: "text",
|
|
2479
|
-
text: JSON.stringify({
|
|
2480
|
-
url: result.url,
|
|
2481
|
-
score: result.score,
|
|
2482
|
-
grade: result.grade,
|
|
2483
|
-
summary: result.summary,
|
|
2484
|
-
// Same paging as the HTTP tool. This copy sliced issues in
|
|
2485
|
-
// detection order and never got the 2026-08-01 worst-first fix.
|
|
2486
|
-
...pageAgentReadyFindings(result, { limit, offset }),
|
|
2487
|
-
duration: result.duration,
|
|
2488
|
-
}, null, 2),
|
|
2489
|
-
},
|
|
2490
|
-
],
|
|
2491
|
-
};
|
|
2492
|
-
});
|
|
2493
|
-
server.tool("competitive_benchmark", "Compare UX across competitor sites. Runs identical cognitive journeys on multiple sites and generates head-to-head comparison with rankings, friction analysis, and recommendations.", {
|
|
2494
|
-
sites: z.array(z.string().url()).describe("Array of URLs to compare"),
|
|
2495
|
-
goal: z.string().describe("Task goal (e.g., 'sign up for free trial')"),
|
|
2496
|
-
persona: z.string().optional().default("first-timer").describe("Persona to use"),
|
|
2497
|
-
maxSteps: z.number().optional().default(30).describe("Max steps per site"),
|
|
2498
|
-
maxTime: z.number().optional().default(180).describe("Max time per site in seconds"),
|
|
2499
|
-
}, async ({ sites, goal, persona, maxSteps, maxTime }) => {
|
|
2500
|
-
const result = await runCompetitiveBenchmark({
|
|
2501
|
-
sites: sites.map((url) => ({ url })),
|
|
2502
|
-
goal,
|
|
2503
|
-
persona,
|
|
2504
|
-
maxSteps,
|
|
2505
|
-
maxTime,
|
|
2506
|
-
headless: true,
|
|
2507
|
-
});
|
|
2508
|
-
return {
|
|
2509
|
-
content: [
|
|
2510
|
-
{
|
|
2511
|
-
type: "text",
|
|
2512
|
-
text: JSON.stringify({
|
|
2513
|
-
goal: result.goal,
|
|
2514
|
-
persona: result.persona,
|
|
2515
|
-
ranking: result.ranking,
|
|
2516
|
-
comparison: result.comparison,
|
|
2517
|
-
recommendations: result.recommendations.slice(0, 5),
|
|
2518
|
-
duration: result.duration,
|
|
2519
|
-
}, null, 2),
|
|
2520
|
-
},
|
|
2521
|
-
],
|
|
2522
|
-
};
|
|
2523
|
-
});
|
|
2524
|
-
// The ui:// resources the canonical tools advertise via _meta.ui. Without
|
|
2525
|
-
// these the stdio server answered "Method not found" to resources/list while
|
|
2526
|
-
// its own tool pointed at ui://cbrowser/empathy, so a UI-capable host was
|
|
2527
|
-
// told a widget existed and could not fetch it.
|
|
2528
|
-
registerUiResources(server);
|
|
2529
|
-
// empathy_audit is registered from the canonical implementation in
|
|
2530
|
-
// mcp-tools/base/audit-tools.ts. This file used to carry its own older copy
|
|
2531
|
-
// of the same tool name -- thin summary, no barrier rects, no persona
|
|
2532
|
-
// weighting, no UI resource -- so the local server answered differently to
|
|
2533
|
-
// the remote one for the same call. Removed 2026-07-31.
|
|
2534
|
-
registerEmpathyAuditTool(server);
|
|
2535
|
-
registerValuesTools(server);
|
|
2536
|
-
// The eight cognitive/comparison tools were registered only on the PUBLIC
|
|
2537
|
-
// path, so the hosted server had them and `npx cbrowser mcp-server` did not:
|
|
2538
|
-
// compare_personas, compare_personas_init, compare_personas_complete,
|
|
2539
|
-
// cognitive_distance, cognitive_coverage, cognitive_interpolate,
|
|
2540
|
-
// cognitive_load_estimate and cognitive_effort. Two of them existed here as
|
|
2541
|
-
// separate older copies, which is how the gap stayed invisible -- the two
|
|
2542
|
-
// most-used names were present locally, so the absence of the other six read
|
|
2543
|
-
// as "those are enterprise features" rather than as a registration gap.
|
|
2544
|
-
// Same split, same fix as the values family earlier today. (2026-08-01)
|
|
2545
|
-
registerPersonaComparisonTools(server, { getBrowser });
|
|
2546
|
-
// The persona-creation family was duplicated here too -- five older copies
|
|
2547
|
-
// against the shared registrar's six. That is why Big Five inference from a
|
|
2548
|
-
// description reached the hosted server and not `npx cbrowser mcp-server`:
|
|
2549
|
-
// the stdio build was answering from tools that predate it. Eighth pair found
|
|
2550
|
-
// this way; the pattern is that anything registered only on the public path
|
|
2551
|
-
// silently diverges from anything hand-written here. (2026-08-01)
|
|
2552
|
-
registerPersonaCreationTools(server);
|
|
2553
|
-
registerPersonaLifecycleTools(server);
|
|
2554
|
-
// =========================================================================
|
|
2555
|
-
// Diagnostics Tools
|
|
2556
|
-
// =========================================================================
|
|
2557
|
-
server.tool("status", "Get CBrowser environment status and diagnostics including data directories, installed browsers, configuration, self-healing cache statistics, and MCP tool count", {}, async () => {
|
|
2558
|
-
// v18.22.0: Include tool count for self-diagnosis of tool discrepancies.
|
|
2559
|
-
// collectedTools sees only server.tool(...) calls, so every family this
|
|
2560
|
-
// server registers through registerTool (empathy_audit, values, persona
|
|
2561
|
-
// comparison/creation/lifecycle) was missing: 55 reported, 82 listed.
|
|
2562
|
-
// Count what tools/list actually serves. (2026-10-07)
|
|
2563
|
-
const info = await getStatusInfo(VERSION, countRegisteredTools(server) ?? collectedTools.length);
|
|
2564
|
-
return {
|
|
2565
|
-
content: [
|
|
2566
|
-
{
|
|
2567
|
-
type: "text",
|
|
2568
|
-
text: JSON.stringify(info, null, 2),
|
|
2569
|
-
},
|
|
2570
|
-
],
|
|
2571
|
-
};
|
|
2572
|
-
});
|
|
2573
|
-
// =========================================================================
|
|
2574
|
-
// Browser Management Tools (v11.8.0)
|
|
2575
|
-
// =========================================================================
|
|
2576
|
-
server.tool("browser_health", "Check if the browser is healthy and responsive. Use this before operations if you suspect the browser may have crashed.", {}, async () => {
|
|
2577
|
-
const b = await getBrowser();
|
|
2578
|
-
const result = await b.isBrowserHealthy();
|
|
2579
|
-
return {
|
|
2580
|
-
content: [
|
|
2581
|
-
{
|
|
2582
|
-
type: "text",
|
|
2583
|
-
text: JSON.stringify(result, null, 2),
|
|
2584
|
-
},
|
|
2585
|
-
],
|
|
2586
|
-
};
|
|
2587
|
-
});
|
|
2588
|
-
server.tool("browser_recover", "Attempt to recover from a browser crash by restarting the browser process. Use this when browser_health returns unhealthy.", {
|
|
2589
|
-
restoreUrl: z.string().url().optional().describe("URL to restore after recovery (uses last known URL if not provided)"),
|
|
2590
|
-
maxAttempts: z.number().optional().default(3).describe("Maximum recovery attempts"),
|
|
2591
|
-
}, async ({ restoreUrl, maxAttempts }) => {
|
|
2592
|
-
const b = await getBrowser();
|
|
2593
|
-
const result = await b.recoverBrowser({ restoreUrl, maxAttempts });
|
|
2594
|
-
return {
|
|
2595
|
-
content: [
|
|
2596
|
-
{
|
|
2597
|
-
type: "text",
|
|
2598
|
-
text: JSON.stringify(result, null, 2),
|
|
2599
|
-
},
|
|
2600
|
-
],
|
|
2601
|
-
};
|
|
2602
|
-
});
|
|
2603
|
-
server.tool("reset_browser", "Reset the browser to a clean state. Clears all cookies, localStorage, sessionStorage, and browser state. Use this when you need a fresh browser environment.", {}, async () => {
|
|
2604
|
-
const b = await getBrowser();
|
|
2605
|
-
await b.reset();
|
|
2606
|
-
// Relaunch for immediate use
|
|
2607
|
-
await b.launch();
|
|
2608
|
-
return {
|
|
2609
|
-
content: [
|
|
2610
|
-
{
|
|
2611
|
-
type: "text",
|
|
2612
|
-
text: JSON.stringify({
|
|
2613
|
-
success: true,
|
|
2614
|
-
message: "Browser reset to clean state and relaunched",
|
|
2615
|
-
}, null, 2),
|
|
2616
|
-
},
|
|
2617
|
-
],
|
|
2618
|
-
};
|
|
2619
|
-
});
|
|
2620
|
-
// =========================================================================
|
|
2621
535
|
// Emotional State Manipulation Tools (v13.1.0)
|
|
2622
536
|
// =========================================================================
|
|
2623
537
|
server.tool("get_emotional_state", "Get the current emotional state of a cognitive journey. Returns the dominant emotion, valence, arousal, and all emotion intensities.", {
|
|
@@ -2715,39 +629,9 @@ Begin the simulation now. Narrate your thoughts as this persona.
|
|
|
2715
629
|
],
|
|
2716
630
|
};
|
|
2717
631
|
});
|
|
2718
|
-
//
|
|
2719
|
-
//
|
|
2720
|
-
|
|
2721
|
-
// =========================================================================
|
|
2722
|
-
// Security Tools (mcp-guardian integration)
|
|
2723
|
-
// =========================================================================
|
|
2724
|
-
server.tool("security_audit", "Audit MCP tool definitions for potential prompt injection attacks. Scans tool descriptions for cross-tool instructions, privilege escalation attempts, and data exfiltration patterns. Returns detailed report of any security issues found.", {
|
|
2725
|
-
config_path: z
|
|
2726
|
-
.string()
|
|
2727
|
-
.optional()
|
|
2728
|
-
.describe("Path to claude_desktop_config.json. If not provided, scans the current CBrowser server's tools."),
|
|
2729
|
-
format: z
|
|
2730
|
-
.enum(["json", "text"])
|
|
2731
|
-
.optional()
|
|
2732
|
-
.default("json")
|
|
2733
|
-
.describe("Output format: json (structured) or text (human-readable)"),
|
|
2734
|
-
async_scan: z
|
|
2735
|
-
.boolean()
|
|
2736
|
-
.optional()
|
|
2737
|
-
.default(false)
|
|
2738
|
-
.describe("If true, connects to MCP servers to scan their tools (slower but more accurate)."),
|
|
2739
|
-
}, async (params) => {
|
|
2740
|
-
// If no config_path provided, scan CBrowser's own tools
|
|
2741
|
-
const options = {
|
|
2742
|
-
...params,
|
|
2743
|
-
// Pass collected CBrowser tools for self-scan when no config_path
|
|
2744
|
-
...(params.config_path ? {} : {
|
|
2745
|
-
tools: collectedTools.map(t => ({ name: t.name, description: t.description, schema: {} })),
|
|
2746
|
-
serverName: "cbrowser",
|
|
2747
|
-
}),
|
|
2748
|
-
};
|
|
2749
|
-
return await securityAuditHandler(options);
|
|
2750
|
-
});
|
|
632
|
+
// security_audit's self-scan reads this list. Without it the shared tool
|
|
633
|
+
// falls back to fetching localhost:$PORT/mcp, which is not this process.
|
|
634
|
+
setSecurityAuditToolList(listRegisteredTools(server));
|
|
2751
635
|
return server;
|
|
2752
636
|
}
|
|
2753
637
|
/**
|