cbrowser 19.5.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.
Files changed (187) hide show
  1. package/README.md +292 -232
  2. package/THIRD_PARTY_LICENSES.md +455 -7
  3. package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
  4. package/dist/analysis/accessibility-empathy.js +2 -0
  5. package/dist/analysis/accessibility-empathy.js.map +1 -1
  6. package/dist/analysis/competitive-benchmark.d.ts.map +1 -1
  7. package/dist/analysis/competitive-benchmark.js +70 -558
  8. package/dist/analysis/competitive-benchmark.js.map +1 -1
  9. package/dist/analysis/natural-language.d.ts.map +1 -1
  10. package/dist/analysis/natural-language.js +43 -0
  11. package/dist/analysis/natural-language.js.map +1 -1
  12. package/dist/analysis/persona-comparison.d.ts +7 -0
  13. package/dist/analysis/persona-comparison.d.ts.map +1 -1
  14. package/dist/analysis/persona-comparison.js +8 -1
  15. package/dist/analysis/persona-comparison.js.map +1 -1
  16. package/dist/browser-options.d.ts +23 -0
  17. package/dist/browser-options.d.ts.map +1 -0
  18. package/dist/browser-options.js +24 -0
  19. package/dist/browser-options.js.map +1 -0
  20. package/dist/browser.d.ts +36 -14
  21. package/dist/browser.d.ts.map +1 -1
  22. package/dist/browser.js +172 -202
  23. package/dist/browser.js.map +1 -1
  24. package/dist/cli-args.d.ts +9 -1
  25. package/dist/cli-args.d.ts.map +1 -1
  26. package/dist/cli-args.js +35 -7
  27. package/dist/cli-args.js.map +1 -1
  28. package/dist/cli-browser-flags.d.ts +33 -0
  29. package/dist/cli-browser-flags.d.ts.map +1 -0
  30. package/dist/cli-browser-flags.js +89 -0
  31. package/dist/cli-browser-flags.js.map +1 -0
  32. package/dist/cli.js +1058 -478
  33. package/dist/cli.js.map +1 -1
  34. package/dist/cognitive/index.d.ts +5 -13
  35. package/dist/cognitive/index.d.ts.map +1 -1
  36. package/dist/cognitive/index.js +44 -15
  37. package/dist/cognitive/index.js.map +1 -1
  38. package/dist/config.d.ts +9 -0
  39. package/dist/config.d.ts.map +1 -1
  40. package/dist/config.js +16 -6
  41. package/dist/config.js.map +1 -1
  42. package/dist/daemon.d.ts +9 -5
  43. package/dist/daemon.d.ts.map +1 -1
  44. package/dist/daemon.js +102 -36
  45. package/dist/daemon.js.map +1 -1
  46. package/dist/device-names.d.ts +10 -0
  47. package/dist/device-names.d.ts.map +1 -0
  48. package/dist/device-names.js +16 -0
  49. package/dist/device-names.js.map +1 -0
  50. package/dist/device-presets-devtools.d.ts +3 -0
  51. package/dist/device-presets-devtools.d.ts.map +1 -0
  52. package/dist/device-presets-devtools.js +823 -0
  53. package/dist/device-presets-devtools.js.map +1 -0
  54. package/dist/devices.d.ts +35 -0
  55. package/dist/devices.d.ts.map +1 -0
  56. package/dist/devices.js +214 -0
  57. package/dist/devices.js.map +1 -0
  58. package/dist/eval-script.d.ts +29 -0
  59. package/dist/eval-script.d.ts.map +1 -0
  60. package/dist/eval-script.js +46 -0
  61. package/dist/eval-script.js.map +1 -0
  62. package/dist/help-client.d.ts +48 -0
  63. package/dist/help-client.d.ts.map +1 -0
  64. package/dist/help-client.js +96 -0
  65. package/dist/help-client.js.map +1 -0
  66. package/dist/index.d.ts +1 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +1 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/mcp-server.d.ts.map +1 -1
  71. package/dist/mcp-server.js +165 -2281
  72. package/dist/mcp-server.js.map +1 -1
  73. package/dist/mcp-tools/base/advanced-interaction-tools.d.ts.map +1 -1
  74. package/dist/mcp-tools/base/advanced-interaction-tools.js +21 -21
  75. package/dist/mcp-tools/base/advanced-interaction-tools.js.map +1 -1
  76. package/dist/mcp-tools/base/assertion-tools.d.ts.map +1 -1
  77. package/dist/mcp-tools/base/assertion-tools.js +6 -3
  78. package/dist/mcp-tools/base/assertion-tools.js.map +1 -1
  79. package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
  80. package/dist/mcp-tools/base/audit-tools.js +15 -4
  81. package/dist/mcp-tools/base/audit-tools.js.map +1 -1
  82. package/dist/mcp-tools/base/browser-state-tools.d.ts.map +1 -1
  83. package/dist/mcp-tools/base/browser-state-tools.js +33 -27
  84. package/dist/mcp-tools/base/browser-state-tools.js.map +1 -1
  85. package/dist/mcp-tools/base/bug-analysis-tools.d.ts.map +1 -1
  86. package/dist/mcp-tools/base/bug-analysis-tools.js +9 -2
  87. package/dist/mcp-tools/base/bug-analysis-tools.js.map +1 -1
  88. package/dist/mcp-tools/base/capture-tools.d.ts +60 -4
  89. package/dist/mcp-tools/base/capture-tools.d.ts.map +1 -1
  90. package/dist/mcp-tools/base/capture-tools.js +118 -14
  91. package/dist/mcp-tools/base/capture-tools.js.map +1 -1
  92. package/dist/mcp-tools/base/index.d.ts +6 -5
  93. package/dist/mcp-tools/base/index.d.ts.map +1 -1
  94. package/dist/mcp-tools/base/index.js +7 -6
  95. package/dist/mcp-tools/base/index.js.map +1 -1
  96. package/dist/mcp-tools/base/interaction-tools.d.ts.map +1 -1
  97. package/dist/mcp-tools/base/interaction-tools.js +20 -6
  98. package/dist/mcp-tools/base/interaction-tools.js.map +1 -1
  99. package/dist/mcp-tools/base/navigation-tools.d.ts +1 -1
  100. package/dist/mcp-tools/base/navigation-tools.d.ts.map +1 -1
  101. package/dist/mcp-tools/base/navigation-tools.js +83 -3
  102. package/dist/mcp-tools/base/navigation-tools.js.map +1 -1
  103. package/dist/mcp-tools/base/site-knowledge-tools.d.ts.map +1 -1
  104. package/dist/mcp-tools/base/site-knowledge-tools.js +11 -23
  105. package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
  106. package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
  107. package/dist/mcp-tools/base/visual-testing-tools.js +13 -3
  108. package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
  109. package/dist/mcp-tools/enterprise-stubs.d.ts +2 -2
  110. package/dist/mcp-tools/enterprise-stubs.d.ts.map +1 -1
  111. package/dist/mcp-tools/enterprise-stubs.js +29 -16
  112. package/dist/mcp-tools/enterprise-stubs.js.map +1 -1
  113. package/dist/mcp-tools/index.d.ts +19 -14
  114. package/dist/mcp-tools/index.d.ts.map +1 -1
  115. package/dist/mcp-tools/index.js +2 -2
  116. package/dist/mcp-tools/index.js.map +1 -1
  117. package/dist/mcp-tools/red-zone-refusal.d.ts +33 -0
  118. package/dist/mcp-tools/red-zone-refusal.d.ts.map +1 -0
  119. package/dist/mcp-tools/red-zone-refusal.js +43 -0
  120. package/dist/mcp-tools/red-zone-refusal.js.map +1 -0
  121. package/dist/mcp-tools/tier-gate.js +1 -1
  122. package/dist/mcp-tools/tier-gate.js.map +1 -1
  123. package/dist/mcp-tools/tool-categories.d.ts.map +1 -1
  124. package/dist/mcp-tools/tool-categories.js +2 -0
  125. package/dist/mcp-tools/tool-categories.js.map +1 -1
  126. package/dist/output.d.ts.map +1 -1
  127. package/dist/output.js +18 -2
  128. package/dist/output.js.map +1 -1
  129. package/dist/performance/metrics.d.ts +8 -3
  130. package/dist/performance/metrics.d.ts.map +1 -1
  131. package/dist/performance/metrics.js +16 -5
  132. package/dist/performance/metrics.js.map +1 -1
  133. package/dist/recording/contact-sheet.js.map +1 -1
  134. package/dist/security/tool-permissions.d.ts.map +1 -1
  135. package/dist/security/tool-permissions.js +1 -0
  136. package/dist/security/tool-permissions.js.map +1 -1
  137. package/dist/testing/coverage.d.ts +2 -2
  138. package/dist/testing/coverage.d.ts.map +1 -1
  139. package/dist/testing/coverage.js +6 -2
  140. package/dist/testing/coverage.js.map +1 -1
  141. package/dist/testing/flaky-detection.d.ts +2 -2
  142. package/dist/testing/flaky-detection.d.ts.map +1 -1
  143. package/dist/testing/flaky-detection.js +11 -3
  144. package/dist/testing/flaky-detection.js.map +1 -1
  145. package/dist/testing/nl-test-suite.d.ts +20 -0
  146. package/dist/testing/nl-test-suite.d.ts.map +1 -1
  147. package/dist/testing/nl-test-suite.js +7 -1
  148. package/dist/testing/nl-test-suite.js.map +1 -1
  149. package/dist/testing/suite-results.d.ts +27 -0
  150. package/dist/testing/suite-results.d.ts.map +1 -0
  151. package/dist/testing/suite-results.js +54 -0
  152. package/dist/testing/suite-results.js.map +1 -0
  153. package/dist/testing/test-repair.d.ts +2 -2
  154. package/dist/testing/test-repair.d.ts.map +1 -1
  155. package/dist/testing/test-repair.js +6 -2
  156. package/dist/testing/test-repair.js.map +1 -1
  157. package/dist/types.d.ts +61 -8
  158. package/dist/types.d.ts.map +1 -1
  159. package/dist/types.js +17 -103
  160. package/dist/types.js.map +1 -1
  161. package/dist/visual/ab-comparison.d.ts.map +1 -1
  162. package/dist/visual/ab-comparison.js +7 -2
  163. package/dist/visual/ab-comparison.js.map +1 -1
  164. package/dist/visual/cross-browser.d.ts +7 -2
  165. package/dist/visual/cross-browser.d.ts.map +1 -1
  166. package/dist/visual/cross-browser.js +33 -5
  167. package/dist/visual/cross-browser.js.map +1 -1
  168. package/dist/visual/distance-metrics.d.ts +7 -0
  169. package/dist/visual/distance-metrics.d.ts.map +1 -1
  170. package/dist/visual/distance-metrics.js.map +1 -1
  171. package/dist/visual/heatmap-overlay.js.map +1 -1
  172. package/dist/visual/llm-relevance.d.ts.map +1 -1
  173. package/dist/visual/llm-relevance.js.map +1 -1
  174. package/dist/visual/regression.d.ts +17 -7
  175. package/dist/visual/regression.d.ts.map +1 -1
  176. package/dist/visual/regression.js +77 -38
  177. package/dist/visual/regression.js.map +1 -1
  178. package/dist/visual/responsive.d.ts +22 -0
  179. package/dist/visual/responsive.d.ts.map +1 -1
  180. package/dist/visual/responsive.js +44 -7
  181. package/dist/visual/responsive.js.map +1 -1
  182. package/dist/visual/visual-overlays.js.map +1 -1
  183. package/dist/webhooks.d.ts +67 -0
  184. package/dist/webhooks.d.ts.map +1 -0
  185. package/dist/webhooks.js +128 -0
  186. package/dist/webhooks.js.map +1 -0
  187. package/package.json +23 -24
@@ -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, getStatusInfo } from "./config.js";
19
- // Screen capture tools — implementation shared with the remote MCP server
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 { countRegisteredTools } from "./mcp-tools/base/browser-management-tools.js";
28
- import { applySecurityLayer } from "./mcp-tools/security-layer.js";
29
- // Visual module imports
30
- import { runVisualRegression, runCrossBrowserTest, runResponsiveTest, runABComparison, crossBrowserDiff, captureVisualBaseline, listVisualBaselines, listSmartBaselineSummaries, } from "./visual/index.js";
31
- // Testing module imports
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
- // Calculate averages
2040
- const avgTime = session.results.reduce((sum, r) => sum + r.timeElapsed, 0) / session.results.length;
2041
- const avgSteps = session.results.reduce((sum, r) => sum + r.stepCount, 0) / session.results.length;
2042
- const summary = {
2043
- sessionId,
2044
- url: session.url,
2045
- goal: session.goal,
2046
- timestamp: new Date().toISOString(),
2047
- totalPersonas: session.personas.length,
2048
- successCount: successfulResults.length,
2049
- failureCount: failedResults.length,
2050
- successRate: `${Math.round((successfulResults.length / session.results.length) * 100)}%`,
2051
- fastestPersona: sortedByTime[0]?.persona || "N/A",
2052
- slowestPersona: sortedByTime[sortedByTime.length - 1]?.persona || "N/A",
2053
- fewestSteps: sortedBySteps[0]?.persona || "N/A",
2054
- mostFriction: sortedByFriction[0]?.persona || "N/A",
2055
- leastFriction: sortedByFriction[sortedByFriction.length - 1]?.persona || "N/A",
2056
- avgCompletionTime: Math.round(avgTime),
2057
- avgSteps: Math.round(avgSteps),
2058
- commonFrictionPoints: commonFriction,
2059
- recommendations,
2060
- results: session.results.map(r => ({
2061
- persona: r.persona,
2062
- success: r.goalAchieved,
2063
- abandonmentReason: r.abandonmentReason,
2064
- timeElapsed: r.timeElapsed,
2065
- stepCount: r.stepCount,
2066
- frictionCount: r.frictionPoints.length,
2067
- finalPatience: Math.round(r.finalState.patienceRemaining * 100) + "%",
2068
- finalFrustration: Math.round(r.finalState.frustrationLevel * 100) + "%",
2069
- finalConfusion: Math.round(r.finalState.confusionLevel * 100) + "%",
2070
- })),
2071
- };
2072
- // Clean up session after summarizing
2073
- comparisonSessions.delete(sessionId);
2074
- return {
2075
- content: [
2076
- {
2077
- type: "text",
2078
- text: JSON.stringify(summary, null, 2),
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
- // Persona Creation Tools (v17.3.0)
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
  /**