cbrowser 10.5.2 → 10.7.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.
@@ -17,7 +17,7 @@ import { runVisualRegression, runCrossBrowserTest, runResponsiveTest, runABCompa
17
17
  import { runNLTestSuite, parseNLTestSuite, dryRunNLTestSuite, repairTest, detectFlakyTests, generateCoverageMap, } from "./testing/index.js";
18
18
  // Analysis module imports
19
19
  import { huntBugs, runChaosTest, comparePersonas, findElementByIntent, runAgentReadyAudit, runCompetitiveBenchmark, runEmpathyAudit, } from "./analysis/index.js";
20
- import { listAccessibilityPersonas } from "./personas.js";
20
+ import { listAccessibilityPersonas, getAccessibilityPersona } from "./personas.js";
21
21
  // Persona imports for cognitive journey
22
22
  import { getPersona, listPersonas, getCognitiveProfile, createCognitivePersona, } from "./personas.js";
23
23
  // Performance module imports
@@ -37,6 +37,52 @@ async function getBrowser() {
37
37
  }
38
38
  // Session storage (in-memory, cleared when server restarts)
39
39
  const comparisonSessions = new Map();
40
+ // WCAG criteria reference for barrier mapping
41
+ const WCAG_CRITERIA = {
42
+ "1.1.1": { level: "A", description: "Non-text Content" },
43
+ "1.3.1": { level: "A", description: "Info and Relationships" },
44
+ "1.4.1": { level: "A", description: "Use of Color" },
45
+ "1.4.3": { level: "AA", description: "Contrast (Minimum)" },
46
+ "1.4.4": { level: "AA", description: "Resize Text" },
47
+ "1.4.6": { level: "AAA", description: "Contrast (Enhanced)" },
48
+ "1.4.10": { level: "AA", description: "Reflow" },
49
+ "2.1.1": { level: "A", description: "Keyboard" },
50
+ "2.1.2": { level: "A", description: "No Keyboard Trap" },
51
+ "2.2.1": { level: "A", description: "Timing Adjustable" },
52
+ "2.2.2": { level: "A", description: "Pause, Stop, Hide" },
53
+ "2.4.1": { level: "A", description: "Bypass Blocks" },
54
+ "2.4.3": { level: "A", description: "Focus Order" },
55
+ "2.4.6": { level: "AA", description: "Headings and Labels" },
56
+ "2.4.7": { level: "AA", description: "Focus Visible" },
57
+ "2.5.5": { level: "AAA", description: "Target Size" },
58
+ "2.5.8": { level: "AA", description: "Target Size (Minimum)" },
59
+ "3.3.1": { level: "A", description: "Error Identification" },
60
+ "3.3.2": { level: "A", description: "Labels or Instructions" },
61
+ "4.1.2": { level: "A", description: "Name, Role, Value" },
62
+ };
63
+ function getWcagCriteriaForBarrier(barrierType) {
64
+ switch (barrierType) {
65
+ case "motor_precision":
66
+ return ["2.5.5", "2.5.8"];
67
+ case "visual_clarity":
68
+ return ["1.4.3", "1.4.6", "1.4.4"];
69
+ case "cognitive_load":
70
+ return ["2.4.6", "3.3.2"];
71
+ case "temporal":
72
+ return ["2.2.1", "2.2.2"];
73
+ case "sensory":
74
+ return ["1.1.1", "1.4.1"];
75
+ case "contrast":
76
+ return ["1.4.3", "1.4.6"];
77
+ case "touch_target":
78
+ return ["2.5.5", "2.5.8"];
79
+ case "timing":
80
+ return ["2.2.1", "2.2.2"];
81
+ default:
82
+ return [];
83
+ }
84
+ }
85
+ const empathyAuditSessions = new Map();
40
86
  // Cleanup old sessions (older than 1 hour)
41
87
  function cleanupOldSessions() {
42
88
  const oneHourAgo = Date.now() - 60 * 60 * 1000;
@@ -45,6 +91,117 @@ function cleanupOldSessions() {
45
91
  comparisonSessions.delete(id);
46
92
  }
47
93
  }
94
+ for (const [id, session] of empathyAuditSessions) {
95
+ if (session.createdAt < oneHourAgo) {
96
+ empathyAuditSessions.delete(id);
97
+ }
98
+ }
99
+ }
100
+ // Helper: Get barrier hints based on persona traits
101
+ function getBarrierHintsForPersona(persona) {
102
+ const hints = [];
103
+ const traits = persona.accessibilityTraits;
104
+ if (traits?.motorControl !== undefined && traits.motorControl < 0.5) {
105
+ hints.push("Watch for small click targets (<44px), precise hover requirements, drag-and-drop interactions");
106
+ }
107
+ if (traits?.tremor) {
108
+ hints.push("Test for accidental double-clicks, cursor jitter tolerance, need for 'undo' options");
109
+ }
110
+ if (traits?.visionLevel !== undefined && traits.visionLevel < 0.5) {
111
+ hints.push("Check contrast ratios, text scaling support, zoom behavior at 200-300%");
112
+ }
113
+ if (traits?.colorBlindness) {
114
+ hints.push(`Check for color-only information (${traits.colorBlindness} colorblindness), ensure status indicators have non-color cues`);
115
+ }
116
+ if (traits?.processingSpeed !== undefined && traits.processingSpeed < 0.5) {
117
+ hints.push("Watch for time limits, auto-advancing content, complex multi-step processes");
118
+ }
119
+ if (traits?.attentionSpan !== undefined && traits.attentionSpan < 0.5) {
120
+ hints.push("Note distracting animations, long forms, lack of progress indicators");
121
+ }
122
+ // Check for hearing-related disability
123
+ const disabilityType = persona.disabilityType || "";
124
+ const personaName = persona.name || "";
125
+ if (disabilityType.includes("hearing") || disabilityType.includes("deaf") || personaName.includes("deaf") || personaName.includes("hearing")) {
126
+ hints.push("Check for audio-only content, video captions, visual alerts for audio notifications");
127
+ }
128
+ if (hints.length === 0) {
129
+ hints.push("Observe general usability and any unexpected difficulties");
130
+ }
131
+ return hints;
132
+ }
133
+ // Helper: Get remediation suggestion for barrier type
134
+ function getRemediationForBarrier(barrierType, element) {
135
+ const remediations = {
136
+ motor_precision: `Increase target size to at least 44x44px for "${element}". Add generous padding and spacing.`,
137
+ visual_clarity: `Improve contrast ratio to at least 4.5:1 for "${element}". Ensure text scales properly.`,
138
+ cognitive_load: `Simplify "${element}" - reduce options, add clear labels, provide inline help.`,
139
+ temporal: `Remove or extend time limits on "${element}". Allow users to pause/extend deadlines.`,
140
+ sensory: `Add text alternative for "${element}". Don't rely on color alone to convey information.`,
141
+ contrast: `Increase contrast ratio for "${element}" to at least 4.5:1 (3:1 for large text).`,
142
+ touch_target: `Increase touch target size for "${element}" to minimum 44x44px (WCAG 2.5.8).`,
143
+ timing: `Extend or remove timing constraints on "${element}". Provide pause/stop controls.`,
144
+ };
145
+ return remediations[barrierType] || `Review "${element}" for accessibility improvements.`;
146
+ }
147
+ // Helper: Derive disability type from persona traits
148
+ function getDisabilityTypeFromPersona(persona) {
149
+ const traits = persona.accessibilityTraits;
150
+ if (traits?.tremor)
151
+ return "Motor impairment (tremor)";
152
+ if (traits?.visionLevel !== undefined && traits.visionLevel < 0.5)
153
+ return "Low vision";
154
+ if (traits?.colorBlindness)
155
+ return `Color blindness (${traits.colorBlindness})`;
156
+ if (persona.cognitiveTraits?.workingMemory !== undefined && persona.cognitiveTraits.workingMemory < 0.5)
157
+ return "Cognitive (ADHD/Memory)";
158
+ if (traits?.processingSpeed !== undefined && traits.processingSpeed < 0.6)
159
+ return "Cognitive (Processing)";
160
+ // Fallback to name-based detection
161
+ if (persona.name.includes("deaf") || persona.name.includes("hearing"))
162
+ return "Hearing impairment";
163
+ if (persona.name.includes("motor"))
164
+ return "Motor impairment";
165
+ if (persona.name.includes("vision") || persona.name.includes("blind"))
166
+ return "Vision impairment";
167
+ if (persona.name.includes("cognitive") || persona.name.includes("adhd"))
168
+ return "Cognitive";
169
+ if (persona.name.includes("elderly"))
170
+ return "Age-related impairments";
171
+ if (persona.name.includes("dyslexic"))
172
+ return "Dyslexia";
173
+ return "General accessibility";
174
+ }
175
+ // Helper: Generate recommendations from empathy audit
176
+ function generateEmpathyRecommendations(session) {
177
+ const recommendations = [];
178
+ // Check success rate
179
+ const successRate = session.personaResults.filter(r => r.goalAchieved).length / session.personaResults.length;
180
+ if (successRate < 0.5) {
181
+ recommendations.push("CRITICAL: Less than 50% of disability personas could complete the goal. Fundamental accessibility improvements needed.");
182
+ }
183
+ else if (successRate < 0.8) {
184
+ recommendations.push("Several disability personas struggled to complete the goal. Review barriers by persona type.");
185
+ }
186
+ // Check for critical barriers
187
+ const criticalBarriers = session.barriers.filter(b => b.severity === "critical");
188
+ if (criticalBarriers.length > 0) {
189
+ recommendations.push(`${criticalBarriers.length} critical barriers found. Address these first as they prevent task completion.`);
190
+ }
191
+ // Check WCAG violations by level
192
+ const levelAViolations = Array.from(session.wcagViolations).filter(c => WCAG_CRITERIA[c]?.level === "A");
193
+ if (levelAViolations.length > 0) {
194
+ recommendations.push(`${levelAViolations.length} WCAG Level A violations (minimum compliance). These are legally required in most jurisdictions.`);
195
+ }
196
+ // Persona-specific recommendations
197
+ const worstPersona = session.personaResults.sort((a, b) => a.empathyScore - b.empathyScore)[0];
198
+ if (worstPersona && worstPersona.empathyScore < 50) {
199
+ recommendations.push(`"${worstPersona.persona}" (${worstPersona.disabilityType}) had the worst experience (score: ${worstPersona.empathyScore}). Prioritize improvements for this user group.`);
200
+ }
201
+ if (recommendations.length === 0) {
202
+ recommendations.push("Good accessibility foundation. Continue testing with real users with disabilities for deeper insights.");
203
+ }
204
+ return recommendations;
48
205
  }
49
206
  export async function startMcpServer() {
50
207
  // Auto-initialize all data directories on server start
@@ -697,29 +854,56 @@ export async function startMcpServer() {
697
854
  ],
698
855
  };
699
856
  });
700
- server.tool("compare_personas", "Compare how different user personas experience a journey", {
857
+ server.tool("compare_personas", "Compare how different user personas experience a journey. REQUIRES API KEY for internal simulation. For API-free usage over remote MCP, use compare_personas_init + browser tools + compare_personas_record_result + compare_personas_summarize instead.", {
701
858
  url: z.string().url().describe("Starting URL"),
702
859
  goal: z.string().describe("Goal to accomplish"),
703
860
  personas: z.array(z.string()).describe("Persona names to compare"),
704
861
  }, async ({ url, goal, personas }) => {
705
- const result = await comparePersonas({
706
- startUrl: url,
707
- goal,
708
- personas,
709
- });
710
- return {
711
- content: [
712
- {
713
- type: "text",
714
- text: JSON.stringify({
715
- url: result.url,
716
- goal: result.goal,
717
- personasCompared: result.personas.length,
718
- summary: result.summary,
719
- }, null, 2),
720
- },
721
- ],
722
- };
862
+ try {
863
+ const result = await comparePersonas({
864
+ startUrl: url,
865
+ goal,
866
+ personas,
867
+ });
868
+ return {
869
+ content: [
870
+ {
871
+ type: "text",
872
+ text: JSON.stringify({
873
+ url: result.url,
874
+ goal: result.goal,
875
+ personasCompared: result.personas.length,
876
+ summary: result.summary,
877
+ }, null, 2),
878
+ },
879
+ ],
880
+ };
881
+ }
882
+ catch (error) {
883
+ const errorMessage = error instanceof Error ? error.message : String(error);
884
+ if (errorMessage.includes("API key")) {
885
+ return {
886
+ content: [
887
+ {
888
+ type: "text",
889
+ text: JSON.stringify({
890
+ error: "API key required for all-in-one compare_personas",
891
+ solution: "Use the API-free session bridge pattern instead:",
892
+ steps: [
893
+ "1. Call compare_personas_init with url, goal, personas",
894
+ "2. For each persona, use browser tools (navigate, click, fill) to attempt the goal",
895
+ "3. Call cognitive_journey_update_state after each action to track cognitive state",
896
+ "4. Call compare_personas_record_result when each persona completes (success or abandon)",
897
+ "5. Call compare_personas_summarize to get the comparison report",
898
+ ],
899
+ note: "Claude orchestrates the simulation - no API key needed when YOU are the brain!",
900
+ }, null, 2),
901
+ },
902
+ ],
903
+ };
904
+ }
905
+ throw error;
906
+ }
723
907
  });
724
908
  server.tool("find_element_by_intent", "AI-powered semantic element finding with ARIA-first selector strategy. Prioritizes aria-label > role > semantic HTML > ID > name > class. Returns selectorType, accessibilityScore (0-1), and alternatives. Use verbose=true for enriched failure responses.", {
725
909
  intent: z.string().describe("Natural language description like 'the cheapest product' or 'login form'"),
@@ -1262,6 +1446,274 @@ Begin the simulation now. Narrate your thoughts as this persona.
1262
1446
  };
1263
1447
  });
1264
1448
  // =========================================================================
1449
+ // Empathy Audit Session Bridge (API-free via Claude orchestration)
1450
+ // =========================================================================
1451
+ server.tool("empathy_audit_init", "Initialize an accessibility empathy audit session. Returns disability persona profiles with traits, barrier detection hints, and WCAG criteria. Claude orchestrates the audit using browser tools, then records barriers. NO API KEY NEEDED - Claude is the brain.", {
1452
+ url: z.string().url().describe("URL to audit"),
1453
+ goal: z.string().describe("Task goal (e.g., 'complete checkout')"),
1454
+ disabilities: z.array(z.string()).optional().describe("Disability personas to test. Available: motor-impairment-tremor, low-vision-magnified, cognitive-adhd, dyslexic-user, deaf-user, elderly-low-vision, color-blind-deuteranopia"),
1455
+ wcagLevel: z.enum(["A", "AA", "AAA"]).optional().default("AA").describe("WCAG conformance level to check against"),
1456
+ }, async ({ url, goal, disabilities, wcagLevel }) => {
1457
+ // Cleanup old sessions
1458
+ cleanupOldSessions();
1459
+ // Generate session ID
1460
+ const sessionId = `empathy_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1461
+ // Get disability personas
1462
+ const disabilityList = disabilities || listAccessibilityPersonas();
1463
+ const personas = disabilityList.map(name => {
1464
+ const persona = getAccessibilityPersona(name);
1465
+ if (!persona) {
1466
+ const customPersona = {
1467
+ name,
1468
+ disabilityType: "unknown",
1469
+ description: `Custom disability persona: ${name}`,
1470
+ accessibilityTraits: {},
1471
+ };
1472
+ return customPersona;
1473
+ }
1474
+ // Build the session persona object first, then compute disabilityType
1475
+ const sessionPersona = {
1476
+ name: persona.name,
1477
+ disabilityType: "", // Will be computed below
1478
+ description: persona.description,
1479
+ accessibilityTraits: persona.accessibilityTraits,
1480
+ cognitiveTraits: persona.cognitiveTraits,
1481
+ };
1482
+ // Compute disability type from traits
1483
+ sessionPersona.disabilityType = getDisabilityTypeFromPersona(sessionPersona);
1484
+ return sessionPersona;
1485
+ });
1486
+ // Store session
1487
+ const session = {
1488
+ id: sessionId,
1489
+ url,
1490
+ goal,
1491
+ wcagLevel: wcagLevel || "AA",
1492
+ personas,
1493
+ currentPersonaIndex: 0,
1494
+ barriers: [],
1495
+ wcagViolations: new Set(),
1496
+ personaResults: [],
1497
+ createdAt: Date.now(),
1498
+ };
1499
+ empathyAuditSessions.set(sessionId, session);
1500
+ return {
1501
+ content: [
1502
+ {
1503
+ type: "text",
1504
+ text: JSON.stringify({
1505
+ sessionId,
1506
+ url,
1507
+ goal,
1508
+ wcagLevel: session.wcagLevel,
1509
+ personaCount: personas.length,
1510
+ personas: personas.map(p => ({
1511
+ name: p.name,
1512
+ disabilityType: p.disabilityType,
1513
+ description: p.description,
1514
+ accessibilityTraits: p.accessibilityTraits,
1515
+ barrierHints: getBarrierHintsForPersona(p),
1516
+ })),
1517
+ wcagCriteria: Object.entries(WCAG_CRITERIA)
1518
+ .filter(([_, v]) => {
1519
+ if (session.wcagLevel === "A")
1520
+ return v.level === "A";
1521
+ if (session.wcagLevel === "AA")
1522
+ return v.level === "A" || v.level === "AA";
1523
+ return true; // AAA includes all
1524
+ })
1525
+ .map(([code, v]) => ({ code, ...v })),
1526
+ instructions: "For each persona: 1) Use browser tools to attempt the goal while noting difficulties. 2) Call empathy_audit_record_barrier for each barrier encountered. 3) Call empathy_audit_complete_persona when done. 4) After all personas, call empathy_audit_summarize.",
1527
+ }, null, 2),
1528
+ },
1529
+ ],
1530
+ };
1531
+ });
1532
+ server.tool("empathy_audit_record_barrier", "Record an accessibility barrier found during the empathy audit. Call this when you observe something that would be difficult for the current disability persona.", {
1533
+ sessionId: z.string().describe("Session ID from empathy_audit_init"),
1534
+ persona: z.string().describe("Persona name experiencing this barrier"),
1535
+ barrierType: z.enum(["motor_precision", "visual_clarity", "cognitive_load", "temporal", "sensory", "contrast", "touch_target", "timing"]).describe("Type of accessibility barrier"),
1536
+ element: z.string().describe("CSS selector or description of the problematic element"),
1537
+ description: z.string().describe("Description of the barrier and its impact"),
1538
+ severity: z.enum(["minor", "major", "critical"]).describe("How severely this impacts the user"),
1539
+ }, async ({ sessionId, persona, barrierType, element, description, severity }) => {
1540
+ const session = empathyAuditSessions.get(sessionId);
1541
+ if (!session) {
1542
+ return {
1543
+ content: [{ type: "text", text: JSON.stringify({ error: "Session not found. Call empathy_audit_init first." }) }],
1544
+ };
1545
+ }
1546
+ // Get WCAG criteria for this barrier type
1547
+ const wcagCriteria = getWcagCriteriaForBarrier(barrierType);
1548
+ wcagCriteria.forEach(c => session.wcagViolations.add(c));
1549
+ const barrier = {
1550
+ type: barrierType,
1551
+ element,
1552
+ description,
1553
+ affectedPersonas: [persona],
1554
+ wcagCriteria,
1555
+ severity: severity,
1556
+ remediation: getRemediationForBarrier(barrierType, element),
1557
+ };
1558
+ session.barriers.push(barrier);
1559
+ return {
1560
+ content: [
1561
+ {
1562
+ type: "text",
1563
+ text: JSON.stringify({
1564
+ recorded: true,
1565
+ sessionId,
1566
+ totalBarriers: session.barriers.length,
1567
+ wcagViolations: Array.from(session.wcagViolations),
1568
+ barrier: {
1569
+ type: barrier.type,
1570
+ severity: barrier.severity,
1571
+ wcagCriteria: barrier.wcagCriteria.map(c => ({
1572
+ code: c,
1573
+ description: WCAG_CRITERIA[c]?.description || "Unknown",
1574
+ })),
1575
+ remediation: barrier.remediation,
1576
+ },
1577
+ }, null, 2),
1578
+ },
1579
+ ],
1580
+ };
1581
+ });
1582
+ server.tool("empathy_audit_complete_persona", "Mark a persona's journey as complete. Call this after finishing the audit for one disability persona.", {
1583
+ sessionId: z.string().describe("Session ID from empathy_audit_init"),
1584
+ persona: z.string().describe("Persona name that completed"),
1585
+ goalAchieved: z.boolean().describe("Whether the goal was accomplished"),
1586
+ stepCount: z.number().describe("Number of steps/actions taken"),
1587
+ notes: z.string().optional().describe("Additional observations about this persona's experience"),
1588
+ }, async ({ sessionId, persona, goalAchieved, stepCount, notes }) => {
1589
+ const session = empathyAuditSessions.get(sessionId);
1590
+ if (!session) {
1591
+ return {
1592
+ content: [{ type: "text", text: JSON.stringify({ error: "Session not found." }) }],
1593
+ };
1594
+ }
1595
+ // Get barriers for this persona
1596
+ const personaBarriers = session.barriers.filter(b => b.affectedPersonas.includes(persona));
1597
+ const personaWcag = new Set();
1598
+ personaBarriers.forEach(b => b.wcagCriteria.forEach(c => personaWcag.add(c)));
1599
+ // Calculate empathy score (heuristic)
1600
+ const barrierPenalty = personaBarriers.reduce((sum, b) => {
1601
+ const severityWeight = { minor: 5, major: 15, critical: 30 };
1602
+ return sum + (severityWeight[b.severity] || 10);
1603
+ }, 0);
1604
+ const empathyScore = Math.max(0, Math.min(100, 100 - barrierPenalty - (goalAchieved ? 0 : 20)));
1605
+ const result = {
1606
+ persona,
1607
+ disabilityType: session.personas.find(p => p.name === persona)?.disabilityType || "unknown",
1608
+ goalAchieved,
1609
+ barriers: personaBarriers,
1610
+ wcagViolations: Array.from(personaWcag),
1611
+ stepCount,
1612
+ empathyScore,
1613
+ notes,
1614
+ };
1615
+ session.personaResults.push(result);
1616
+ session.currentPersonaIndex++;
1617
+ const remaining = session.personas.length - session.personaResults.length;
1618
+ return {
1619
+ content: [
1620
+ {
1621
+ type: "text",
1622
+ text: JSON.stringify({
1623
+ recorded: true,
1624
+ sessionId,
1625
+ persona,
1626
+ empathyScore,
1627
+ barriersFound: personaBarriers.length,
1628
+ wcagViolations: result.wcagViolations,
1629
+ completedPersonas: session.personaResults.length,
1630
+ totalPersonas: session.personas.length,
1631
+ remaining,
1632
+ nextStep: remaining > 0
1633
+ ? `Audit ${remaining} more persona(s), then call empathy_audit_summarize`
1634
+ : "All personas complete. Call empathy_audit_summarize for the final report.",
1635
+ }, null, 2),
1636
+ },
1637
+ ],
1638
+ };
1639
+ });
1640
+ server.tool("empathy_audit_summarize", "Generate the final empathy audit summary after all personas have completed. Returns scores, barriers, WCAG violations, and remediation priorities.", {
1641
+ sessionId: z.string().describe("Session ID from empathy_audit_init"),
1642
+ }, async ({ sessionId }) => {
1643
+ const session = empathyAuditSessions.get(sessionId);
1644
+ if (!session) {
1645
+ return {
1646
+ content: [{ type: "text", text: JSON.stringify({ error: "Session not found." }) }],
1647
+ };
1648
+ }
1649
+ if (session.personaResults.length === 0) {
1650
+ return {
1651
+ content: [{ type: "text", text: JSON.stringify({ error: "No persona results recorded. Complete at least one persona journey first." }) }],
1652
+ };
1653
+ }
1654
+ // Calculate overall score
1655
+ const overallScore = Math.round(session.personaResults.reduce((sum, r) => sum + r.empathyScore, 0) / session.personaResults.length);
1656
+ // Determine grade
1657
+ const grade = overallScore >= 90 ? "A" : overallScore >= 80 ? "B" : overallScore >= 70 ? "C" : overallScore >= 60 ? "D" : "F";
1658
+ // Aggregate barriers by type
1659
+ const barriersByType = {};
1660
+ session.barriers.forEach(b => {
1661
+ barriersByType[b.type] = (barriersByType[b.type] || 0) + 1;
1662
+ });
1663
+ // Prioritize remediation
1664
+ const remediationPriority = session.barriers
1665
+ .sort((a, b) => {
1666
+ const severityOrder = { critical: 0, major: 1, minor: 2 };
1667
+ return (severityOrder[a.severity] || 2) - (severityOrder[b.severity] || 2);
1668
+ })
1669
+ .slice(0, 10)
1670
+ .map((b, i) => ({
1671
+ priority: i + 1,
1672
+ type: b.type,
1673
+ element: b.element,
1674
+ severity: b.severity,
1675
+ remediation: b.remediation,
1676
+ wcagCriteria: b.wcagCriteria,
1677
+ }));
1678
+ const summary = {
1679
+ sessionId,
1680
+ url: session.url,
1681
+ goal: session.goal,
1682
+ wcagLevel: session.wcagLevel,
1683
+ timestamp: new Date().toISOString(),
1684
+ overallScore,
1685
+ grade,
1686
+ totalBarriers: session.barriers.length,
1687
+ totalWcagViolations: session.wcagViolations.size,
1688
+ wcagViolations: Array.from(session.wcagViolations).map(c => ({
1689
+ code: c,
1690
+ level: WCAG_CRITERIA[c]?.level || "?",
1691
+ description: WCAG_CRITERIA[c]?.description || "Unknown",
1692
+ })),
1693
+ barriersByType,
1694
+ personaResults: session.personaResults.map(r => ({
1695
+ persona: r.persona,
1696
+ disabilityType: r.disabilityType,
1697
+ goalAchieved: r.goalAchieved,
1698
+ empathyScore: r.empathyScore,
1699
+ barriersFound: r.barriers.length,
1700
+ wcagViolationCount: r.wcagViolations.length,
1701
+ })),
1702
+ remediationPriority,
1703
+ recommendations: generateEmpathyRecommendations(session),
1704
+ };
1705
+ // Clean up session
1706
+ empathyAuditSessions.delete(sessionId);
1707
+ return {
1708
+ content: [
1709
+ {
1710
+ type: "text",
1711
+ text: JSON.stringify(summary, null, 2),
1712
+ },
1713
+ ],
1714
+ };
1715
+ });
1716
+ // =========================================================================
1265
1717
  // Performance Tools (v6.4.0+)
1266
1718
  // =========================================================================
1267
1719
  server.tool("perf_baseline", "Capture performance baseline for a URL", {
@@ -1382,7 +1834,7 @@ Begin the simulation now. Narrate your thoughts as this persona.
1382
1834
  ],
1383
1835
  };
1384
1836
  });
1385
- server.tool("empathy_audit", "Simulate how people with disabilities experience a site. Tests motor impairments, cognitive differences, and sensory limitations. Returns barriers, WCAG violations, and remediation suggestions.", {
1837
+ server.tool("empathy_audit", "Simulate how people with disabilities experience a site. REQUIRES API KEY for internal simulation. For API-free usage over remote MCP, use empathy_audit_init + browser tools + empathy_audit_record_barrier + empathy_audit_complete_persona + empathy_audit_summarize instead.", {
1386
1838
  url: z.string().url().describe("URL to audit"),
1387
1839
  goal: z.string().describe("Task goal (e.g., 'complete checkout')"),
1388
1840
  disabilities: z.array(z.string()).optional().describe("Disability personas to test. Available: motor-impairment-tremor, low-vision-magnified, cognitive-adhd, dyslexic-user, deaf-user, elderly-low-vision, color-blind-deuteranopia"),
@@ -1390,40 +1842,68 @@ Begin the simulation now. Narrate your thoughts as this persona.
1390
1842
  maxSteps: z.number().optional().default(20).describe("Max steps per persona"),
1391
1843
  maxTime: z.number().optional().default(120).describe("Max time per persona in seconds"),
1392
1844
  }, async ({ url, goal, disabilities, wcagLevel, maxSteps, maxTime }) => {
1393
- // Default to all if not specified
1394
- const disabilityList = disabilities || listAccessibilityPersonas();
1395
- const result = await runEmpathyAudit(url, {
1396
- goal,
1397
- disabilities: disabilityList,
1398
- wcagLevel,
1399
- maxSteps,
1400
- maxTime,
1401
- headless: true,
1402
- });
1403
- return {
1404
- content: [
1405
- {
1406
- type: "text",
1407
- text: JSON.stringify({
1408
- url: result.url,
1409
- goal: result.goal,
1410
- overallScore: result.overallScore,
1411
- resultsSummary: result.results.map((r) => ({
1412
- persona: r.persona,
1413
- disabilityType: r.disabilityType,
1414
- goalAchieved: r.goalAchieved,
1415
- empathyScore: r.empathyScore,
1416
- barrierCount: r.barriers.length,
1417
- wcagViolationCount: r.wcagViolations.length,
1418
- })),
1419
- allWcagViolations: result.allWcagViolations,
1420
- topBarriers: result.allBarriers.slice(0, 5),
1421
- topRemediation: result.combinedRemediation.slice(0, 5),
1422
- duration: result.duration,
1423
- }, null, 2),
1424
- },
1425
- ],
1426
- };
1845
+ try {
1846
+ // Default to all if not specified
1847
+ const disabilityList = disabilities || listAccessibilityPersonas();
1848
+ const result = await runEmpathyAudit(url, {
1849
+ goal,
1850
+ disabilities: disabilityList,
1851
+ wcagLevel,
1852
+ maxSteps,
1853
+ maxTime,
1854
+ headless: true,
1855
+ });
1856
+ return {
1857
+ content: [
1858
+ {
1859
+ type: "text",
1860
+ text: JSON.stringify({
1861
+ url: result.url,
1862
+ goal: result.goal,
1863
+ overallScore: result.overallScore,
1864
+ resultsSummary: result.results.map((r) => ({
1865
+ persona: r.persona,
1866
+ disabilityType: r.disabilityType,
1867
+ goalAchieved: r.goalAchieved,
1868
+ empathyScore: r.empathyScore,
1869
+ barrierCount: r.barriers.length,
1870
+ wcagViolationCount: r.wcagViolations.length,
1871
+ })),
1872
+ allWcagViolations: result.allWcagViolations,
1873
+ topBarriers: result.allBarriers.slice(0, 5),
1874
+ topRemediation: result.combinedRemediation.slice(0, 5),
1875
+ duration: result.duration,
1876
+ }, null, 2),
1877
+ },
1878
+ ],
1879
+ };
1880
+ }
1881
+ catch (error) {
1882
+ const errorMessage = error instanceof Error ? error.message : String(error);
1883
+ if (errorMessage.includes("API key")) {
1884
+ return {
1885
+ content: [
1886
+ {
1887
+ type: "text",
1888
+ text: JSON.stringify({
1889
+ error: "API key required for all-in-one empathy_audit",
1890
+ solution: "Use the API-free session bridge pattern instead:",
1891
+ steps: [
1892
+ "1. Call empathy_audit_init with url, goal, disabilities, wcagLevel",
1893
+ "2. For each disability persona, use browser tools to attempt the goal",
1894
+ "3. Call empathy_audit_record_barrier when you observe accessibility barriers",
1895
+ "4. Call empathy_audit_complete_persona when each persona finishes",
1896
+ "5. Call empathy_audit_summarize to get the final audit report",
1897
+ ],
1898
+ note: "Claude orchestrates the audit - no API key needed when YOU are the brain!",
1899
+ availablePersonas: listAccessibilityPersonas(),
1900
+ }, null, 2),
1901
+ },
1902
+ ],
1903
+ };
1904
+ }
1905
+ throw error;
1906
+ }
1427
1907
  });
1428
1908
  // =========================================================================
1429
1909
  // Diagnostics Tools