@elsahafy/ux-mcp-server 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1766 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { readFile } from "fs/promises";
6
+ import { fileURLToPath } from "url";
7
+ import { dirname, join } from "path";
8
+ // Get __dirname equivalent in ES modules
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+ // Knowledge base paths
12
+ const KNOWLEDGE_PATH = join(__dirname, "..", "knowledge");
13
+ const config = {
14
+ name: "ux-mcp-server",
15
+ version: "1.0.0",
16
+ };
17
+ // Helper to load knowledge file
18
+ async function loadKnowledge(filename) {
19
+ const path = join(KNOWLEDGE_PATH, filename);
20
+ const content = await readFile(path, "utf-8");
21
+ return JSON.parse(content);
22
+ }
23
+ // Create MCP server
24
+ const server = new Server({
25
+ name: config.name,
26
+ version: config.version,
27
+ }, {
28
+ capabilities: {
29
+ resources: {},
30
+ tools: {},
31
+ prompts: {},
32
+ },
33
+ });
34
+ // ========================================
35
+ // RESOURCES - Static UX Knowledge
36
+ // ========================================
37
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
38
+ return {
39
+ resources: [
40
+ {
41
+ uri: "ux://accessibility/wcag",
42
+ name: "WCAG 2.1 AA Guidelines",
43
+ description: "Web Content Accessibility Guidelines with code checks and examples",
44
+ mimeType: "application/json",
45
+ },
46
+ {
47
+ uri: "ux://usability/nielsen-heuristics",
48
+ name: "Nielsen's 10 Usability Heuristics",
49
+ description: "Core usability principles with examples and evaluation questions",
50
+ mimeType: "application/json",
51
+ },
52
+ {
53
+ uri: "ux://patterns/ui-patterns",
54
+ name: "UI Patterns Library",
55
+ description: "Common interface patterns for navigation, forms, feedback, and data display",
56
+ mimeType: "application/json",
57
+ },
58
+ {
59
+ uri: "ux://design-systems/tokens",
60
+ name: "Design System Principles",
61
+ description: "Design tokens, atomic design, and component API guidelines",
62
+ mimeType: "application/json",
63
+ },
64
+ {
65
+ uri: "ux://responsive/design",
66
+ name: "Responsive Design Best Practices",
67
+ description: "Mobile-first principles, breakpoints, responsive patterns, and testing guidelines",
68
+ mimeType: "application/json",
69
+ },
70
+ {
71
+ uri: "ux://themes/dark-mode",
72
+ name: "Dark Mode Implementation Guide",
73
+ description: "Dark mode best practices, color considerations, and accessibility",
74
+ mimeType: "application/json",
75
+ },
76
+ {
77
+ uri: "ux://content/error-messages",
78
+ name: "Error Message Library",
79
+ description: "User-friendly error messages for common scenarios with tone guidelines",
80
+ mimeType: "application/json",
81
+ },
82
+ {
83
+ uri: "ux://performance/optimization",
84
+ name: "Performance Best Practices",
85
+ description: "Core Web Vitals, performance optimization, and loading strategies",
86
+ mimeType: "application/json",
87
+ },
88
+ {
89
+ uri: "ux://seo/guidelines",
90
+ name: "SEO Best Practices",
91
+ description: "Search engine optimization, meta tags, structured data, and technical SEO",
92
+ mimeType: "application/json",
93
+ },
94
+ {
95
+ uri: "ux://i18n/patterns",
96
+ name: "Internationalization (i18n) Patterns",
97
+ description: "Guidelines for building multilingual, globally accessible applications",
98
+ mimeType: "application/json",
99
+ },
100
+ {
101
+ uri: "ux://animation/motion",
102
+ name: "Animation & Motion Design",
103
+ description: "Motion design principles, performance, and accessibility for UI animations",
104
+ mimeType: "application/json",
105
+ },
106
+ {
107
+ uri: "ux://react/patterns",
108
+ name: "React Component Patterns",
109
+ description: "Advanced React patterns for composition, state management, and performance",
110
+ mimeType: "application/json",
111
+ },
112
+ ],
113
+ };
114
+ });
115
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
116
+ const uri = request.params.uri;
117
+ let content;
118
+ let description;
119
+ switch (uri) {
120
+ case "ux://accessibility/wcag":
121
+ content = await loadKnowledge("wcag-guidelines.json");
122
+ description = "Complete WCAG 2.1 Level AA guidelines for web accessibility";
123
+ break;
124
+ case "ux://usability/nielsen-heuristics":
125
+ content = await loadKnowledge("nielsen-heuristics.json");
126
+ description = "Jakob Nielsen's 10 usability heuristics for interface design";
127
+ break;
128
+ case "ux://patterns/ui-patterns":
129
+ content = await loadKnowledge("ui-patterns.json");
130
+ description = "Library of proven UI patterns for common use cases";
131
+ break;
132
+ case "ux://design-systems/tokens":
133
+ content = await loadKnowledge("design-tokens.json");
134
+ description = "Design system principles, tokens, and atomic design methodology";
135
+ break;
136
+ case "ux://responsive/design":
137
+ content = await loadKnowledge("responsive-design.json");
138
+ description = "Mobile-first design, breakpoints, and responsive patterns";
139
+ break;
140
+ case "ux://themes/dark-mode":
141
+ content = await loadKnowledge("dark-mode.json");
142
+ description = "Dark mode implementation, color considerations, and accessibility";
143
+ break;
144
+ case "ux://content/error-messages":
145
+ content = await loadKnowledge("error-messages.json");
146
+ description = "User-friendly error messages with tone guidelines and examples";
147
+ break;
148
+ case "ux://performance/optimization":
149
+ content = await loadKnowledge("performance.json");
150
+ description = "Core Web Vitals and performance optimization strategies";
151
+ break;
152
+ case "ux://seo/guidelines":
153
+ content = await loadKnowledge("seo.json");
154
+ description = "SEO best practices, meta tags, and structured data";
155
+ break;
156
+ case "ux://i18n/patterns":
157
+ content = await loadKnowledge("i18n.json");
158
+ description = "Internationalization patterns for multilingual applications";
159
+ break;
160
+ case "ux://animation/motion":
161
+ content = await loadKnowledge("animation.json");
162
+ description = "Motion design principles and accessible animations";
163
+ break;
164
+ case "ux://react/patterns":
165
+ content = await loadKnowledge("react-patterns.json");
166
+ description = "Advanced React component patterns and best practices";
167
+ break;
168
+ default:
169
+ throw new Error(`Unknown resource: ${uri}`);
170
+ }
171
+ return {
172
+ contents: [
173
+ {
174
+ uri,
175
+ mimeType: "application/json",
176
+ text: JSON.stringify(content, null, 2),
177
+ },
178
+ ],
179
+ };
180
+ });
181
+ // ========================================
182
+ // TOOLS - Dynamic UX Operations
183
+ // ========================================
184
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
185
+ return {
186
+ tools: [
187
+ {
188
+ name: "analyze_accessibility",
189
+ description: "Analyze HTML/JSX code for accessibility issues based on WCAG guidelines. Returns specific violations and suggestions.",
190
+ inputSchema: {
191
+ type: "object",
192
+ properties: {
193
+ code: {
194
+ type: "string",
195
+ description: "HTML or JSX code to analyze",
196
+ },
197
+ level: {
198
+ type: "string",
199
+ enum: ["A", "AA", "AAA"],
200
+ description: "WCAG conformance level to check against",
201
+ default: "AA",
202
+ },
203
+ },
204
+ required: ["code"],
205
+ },
206
+ },
207
+ {
208
+ name: "review_usability",
209
+ description: "Review a UI description or code against Nielsen's usability heuristics. Provides ratings and specific recommendations.",
210
+ inputSchema: {
211
+ type: "object",
212
+ properties: {
213
+ description: {
214
+ type: "string",
215
+ description: "Description of the UI or component to review",
216
+ },
217
+ code: {
218
+ type: "string",
219
+ description: "Optional: Code implementation to review",
220
+ },
221
+ focus_heuristics: {
222
+ type: "array",
223
+ items: { type: "number" },
224
+ description: "Optional: Specific heuristics to focus on (1-10)",
225
+ },
226
+ },
227
+ required: ["description"],
228
+ },
229
+ },
230
+ {
231
+ name: "suggest_pattern",
232
+ description: "Suggest appropriate UI pattern for a given use case. Returns pattern details, best practices, and implementation guidance.",
233
+ inputSchema: {
234
+ type: "object",
235
+ properties: {
236
+ use_case: {
237
+ type: "string",
238
+ description: "Description of what you're trying to build (e.g., 'user navigation', 'form validation', 'data display')",
239
+ },
240
+ constraints: {
241
+ type: "string",
242
+ description: "Optional: Specific constraints or requirements",
243
+ },
244
+ },
245
+ required: ["use_case"],
246
+ },
247
+ },
248
+ {
249
+ name: "generate_component_example",
250
+ description: "Generate framework-agnostic HTML/CSS example following UX best practices for a specific pattern.",
251
+ inputSchema: {
252
+ type: "object",
253
+ properties: {
254
+ pattern: {
255
+ type: "string",
256
+ description: "Pattern name (e.g., 'search', 'modal', 'form', 'button', 'data-table')",
257
+ },
258
+ variant: {
259
+ type: "string",
260
+ description: "Optional: Specific variant (e.g., 'primary', 'ghost', 'inline')",
261
+ },
262
+ include_accessibility: {
263
+ type: "boolean",
264
+ description: "Include ARIA attributes and accessibility features",
265
+ default: true,
266
+ },
267
+ },
268
+ required: ["pattern"],
269
+ },
270
+ },
271
+ {
272
+ name: "audit_design_system",
273
+ description: "Audit design system implementation for consistency with best practices. Checks tokens, naming, and structure.",
274
+ inputSchema: {
275
+ type: "object",
276
+ properties: {
277
+ tokens: {
278
+ type: "string",
279
+ description: "JSON or CSS of design tokens to audit",
280
+ },
281
+ type: {
282
+ type: "string",
283
+ enum: ["colors", "spacing", "typography", "all"],
284
+ description: "Type of tokens to audit",
285
+ default: "all",
286
+ },
287
+ },
288
+ required: ["tokens"],
289
+ },
290
+ },
291
+ {
292
+ name: "check_contrast",
293
+ description: "Check color contrast ratio for WCAG compliance. Supports hex, rgb, and named colors.",
294
+ inputSchema: {
295
+ type: "object",
296
+ properties: {
297
+ foreground: {
298
+ type: "string",
299
+ description: "Foreground color (text)",
300
+ },
301
+ background: {
302
+ type: "string",
303
+ description: "Background color",
304
+ },
305
+ level: {
306
+ type: "string",
307
+ enum: ["AA", "AAA"],
308
+ description: "WCAG level to check",
309
+ default: "AA",
310
+ },
311
+ large_text: {
312
+ type: "boolean",
313
+ description: "Is the text large (18pt+ or 14pt+ bold)?",
314
+ default: false,
315
+ },
316
+ },
317
+ required: ["foreground", "background"],
318
+ },
319
+ },
320
+ {
321
+ name: "check_responsive",
322
+ description: "Analyze code for mobile-first principles and responsive design issues. Checks viewport meta, touch targets, and breakpoints.",
323
+ inputSchema: {
324
+ type: "object",
325
+ properties: {
326
+ code: {
327
+ type: "string",
328
+ description: "HTML/CSS code to analyze",
329
+ },
330
+ check_type: {
331
+ type: "string",
332
+ enum: ["all", "viewport", "touch-targets", "breakpoints", "images"],
333
+ description: "Specific responsive aspect to check",
334
+ default: "all",
335
+ },
336
+ },
337
+ required: ["code"],
338
+ },
339
+ },
340
+ {
341
+ name: "suggest_error_message",
342
+ description: "Get user-friendly error message suggestions for specific scenarios. Returns message, tone guidance, and accessibility considerations.",
343
+ inputSchema: {
344
+ type: "object",
345
+ properties: {
346
+ scenario: {
347
+ type: "string",
348
+ description: "Error scenario (e.g., 'invalid email', 'required field', 'payment failed', 'file too large')",
349
+ },
350
+ context: {
351
+ type: "string",
352
+ description: "Optional: Additional context about the error",
353
+ },
354
+ technical_message: {
355
+ type: "string",
356
+ description: "Optional: Technical error message to translate to user-friendly language",
357
+ },
358
+ },
359
+ required: ["scenario"],
360
+ },
361
+ },
362
+ {
363
+ name: "analyze_performance",
364
+ description: "Analyze code for performance issues and Core Web Vitals optimization. Checks resource loading, image optimization, and rendering performance.",
365
+ inputSchema: {
366
+ type: "object",
367
+ properties: {
368
+ code: {
369
+ type: "string",
370
+ description: "HTML/CSS/JS code to analyze",
371
+ },
372
+ check_type: {
373
+ type: "string",
374
+ enum: ["all", "images", "css", "javascript", "loading"],
375
+ description: "Specific performance aspect to check",
376
+ default: "all",
377
+ },
378
+ },
379
+ required: ["code"],
380
+ },
381
+ },
382
+ {
383
+ name: "check_seo",
384
+ description: "Analyze HTML for SEO best practices. Checks meta tags, Open Graph, structured data, and technical SEO elements.",
385
+ inputSchema: {
386
+ type: "object",
387
+ properties: {
388
+ html: {
389
+ type: "string",
390
+ description: "HTML code to analyze",
391
+ },
392
+ url: {
393
+ type: "string",
394
+ description: "Optional: Page URL for context",
395
+ },
396
+ },
397
+ required: ["html"],
398
+ },
399
+ },
400
+ {
401
+ name: "suggest_animation",
402
+ description: "Suggest appropriate animation for a UI interaction. Returns animation type, duration, easing, and implementation guidance.",
403
+ inputSchema: {
404
+ type: "object",
405
+ properties: {
406
+ interaction: {
407
+ type: "string",
408
+ description: "UI interaction to animate (e.g., 'button click', 'modal open', 'list item add', 'page transition')",
409
+ },
410
+ context: {
411
+ type: "string",
412
+ description: "Optional: Additional context about the interaction",
413
+ },
414
+ },
415
+ required: ["interaction"],
416
+ },
417
+ },
418
+ ],
419
+ };
420
+ });
421
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
422
+ const { name, arguments: args } = request.params;
423
+ try {
424
+ switch (name) {
425
+ case "analyze_accessibility":
426
+ return await analyzeAccessibility(args);
427
+ case "review_usability":
428
+ return await reviewUsability(args);
429
+ case "suggest_pattern":
430
+ return await suggestPattern(args);
431
+ case "generate_component_example":
432
+ return await generateComponentExample(args);
433
+ case "audit_design_system":
434
+ return await auditDesignSystem(args);
435
+ case "check_contrast":
436
+ return await checkContrast(args);
437
+ case "check_responsive":
438
+ return await checkResponsive(args);
439
+ case "suggest_error_message":
440
+ return await suggestErrorMessage(args);
441
+ case "analyze_performance":
442
+ return await analyzePerformance(args);
443
+ case "check_seo":
444
+ return await checkSEO(args);
445
+ case "suggest_animation":
446
+ return await suggestAnimation(args);
447
+ default:
448
+ throw new Error(`Unknown tool: ${name}`);
449
+ }
450
+ }
451
+ catch (error) {
452
+ const errorMessage = error instanceof Error ? error.message : String(error);
453
+ return {
454
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
455
+ isError: true,
456
+ };
457
+ }
458
+ });
459
+ // ========================================
460
+ // TOOL IMPLEMENTATIONS
461
+ // ========================================
462
+ async function analyzeAccessibility(args) {
463
+ const wcag = await loadKnowledge("wcag-guidelines.json");
464
+ const code = args.code;
465
+ const level = args.level || "AA";
466
+ const issues = [];
467
+ const suggestions = [];
468
+ // Check for common accessibility issues
469
+ if (/<img(?![^>]*alt=)/i.test(code)) {
470
+ issues.push("❌ Images without alt text (WCAG 1.1.1)");
471
+ suggestions.push("Add alt attribute to all img elements");
472
+ }
473
+ if (/<input(?![^>]*id=|[^>]*aria-label)/i.test(code)) {
474
+ const hasLabel = /<label[^>]*for=/i.test(code);
475
+ if (!hasLabel) {
476
+ issues.push("❌ Form inputs without labels (WCAG 3.3.2)");
477
+ suggestions.push("Associate labels with inputs using for/id or aria-label");
478
+ }
479
+ }
480
+ if (/<button[^>]*>[^<]*<(svg|i|img)/i.test(code)) {
481
+ if (!/<button[^>]*aria-label/i.test(code)) {
482
+ issues.push("❌ Icon-only buttons without aria-label (WCAG 4.1.2)");
483
+ suggestions.push("Add aria-label to buttons containing only icons");
484
+ }
485
+ }
486
+ if (/style="[^"]*outline:\s*none/i.test(code)) {
487
+ issues.push("⚠️ Focus outline removed without replacement (WCAG 2.4.7)");
488
+ suggestions.push("If removing outline, provide alternative focus indicator");
489
+ }
490
+ if (/<div[^>]*(onclick|@click)/i.test(code)) {
491
+ if (!/<div[^>]*(role=|tabindex)/i.test(code)) {
492
+ issues.push("⚠️ Click handlers on non-interactive elements (WCAG 2.1.1)");
493
+ suggestions.push("Use button/a elements or add role and tabindex for keyboard access");
494
+ }
495
+ }
496
+ if (!/<html[^>]*lang=/i.test(code) && /<html/i.test(code)) {
497
+ issues.push("❌ HTML element missing lang attribute (WCAG 3.1.1)");
498
+ suggestions.push("Add lang attribute to html element (e.g., <html lang='en'>)");
499
+ }
500
+ const result = {
501
+ summary: `Found ${issues.length} accessibility issue(s) at WCAG ${level} level`,
502
+ issues,
503
+ suggestions,
504
+ wcag_reference: "See ux://accessibility/wcag for complete guidelines",
505
+ };
506
+ return {
507
+ content: [
508
+ {
509
+ type: "text",
510
+ text: JSON.stringify(result, null, 2),
511
+ },
512
+ ],
513
+ };
514
+ }
515
+ async function reviewUsability(args) {
516
+ const heuristics = await loadKnowledge("nielsen-heuristics.json");
517
+ const description = args.description;
518
+ const code = args.code;
519
+ const focusHeuristics = args.focus_heuristics || [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
520
+ const review = [];
521
+ // Review against selected heuristics
522
+ for (const heuristicId of focusHeuristics) {
523
+ const heuristic = heuristics.heuristics.find((h) => h.id === heuristicId);
524
+ if (!heuristic)
525
+ continue;
526
+ review.push({
527
+ heuristic: `${heuristic.id}. ${heuristic.name}`,
528
+ description: heuristic.description,
529
+ considerations: heuristic.implementation_tips,
530
+ });
531
+ }
532
+ const result = {
533
+ summary: `Usability review against ${focusHeuristics.length} heuristic(s)`,
534
+ component_description: description,
535
+ review,
536
+ next_steps: [
537
+ "Compare your implementation against the considerations",
538
+ "Check for violations of implementation tips",
539
+ "Review examples and anti-patterns in the heuristics",
540
+ ],
541
+ reference: "See ux://usability/nielsen-heuristics for complete heuristics",
542
+ };
543
+ return {
544
+ content: [
545
+ {
546
+ type: "text",
547
+ text: JSON.stringify(result, null, 2),
548
+ },
549
+ ],
550
+ };
551
+ }
552
+ async function suggestPattern(args) {
553
+ const patterns = await loadKnowledge("ui-patterns.json");
554
+ const useCase = args.use_case.toLowerCase();
555
+ const constraints = args.constraints;
556
+ // Simple pattern matching based on keywords
557
+ let suggestedPatterns = [];
558
+ const patternMap = {
559
+ navigation: ["header_navigation", "breadcrumbs", "tabs"],
560
+ form: ["single_column_form", "multi_step_wizard", "inline_validation"],
561
+ search: ["search"],
562
+ modal: ["modal_dialog"],
563
+ notification: ["toast_notification"],
564
+ table: ["data_table"],
565
+ card: ["cards"],
566
+ loading: ["loading_states"],
567
+ upload: ["file_upload"],
568
+ date: ["date_picker"],
569
+ empty: ["empty_states"],
570
+ };
571
+ // Find matching patterns
572
+ for (const [keyword, patternIds] of Object.entries(patternMap)) {
573
+ if (useCase.includes(keyword)) {
574
+ for (const patternId of patternIds) {
575
+ // Search through all categories
576
+ for (const category of Object.values(patterns.patterns)) {
577
+ if (typeof category === "object" && category !== null && patternId in category) {
578
+ suggestedPatterns.push({
579
+ id: patternId,
580
+ category: Object.keys(patterns.patterns).find((k) => patterns.patterns[k] === category),
581
+ pattern: category[patternId],
582
+ });
583
+ }
584
+ }
585
+ }
586
+ }
587
+ }
588
+ if (suggestedPatterns.length === 0) {
589
+ suggestedPatterns.push({
590
+ message: "No specific pattern match found. Browse all patterns at ux://patterns/ui-patterns",
591
+ });
592
+ }
593
+ const result = {
594
+ use_case: useCase,
595
+ constraints: constraints || "None specified",
596
+ suggested_patterns: suggestedPatterns,
597
+ reference: "See ux://patterns/ui-patterns for complete library",
598
+ };
599
+ return {
600
+ content: [
601
+ {
602
+ type: "text",
603
+ text: JSON.stringify(result, null, 2),
604
+ },
605
+ ],
606
+ };
607
+ }
608
+ async function generateComponentExample(args) {
609
+ const pattern = args.pattern.toLowerCase();
610
+ const variant = args.variant;
611
+ const includeA11y = args.include_accessibility !== false;
612
+ let example = "";
613
+ // Generate examples based on pattern
614
+ switch (pattern) {
615
+ case "button":
616
+ example = `<!-- Accessible Button Component -->
617
+ <button
618
+ type="button"
619
+ class="btn btn-${variant || "primary"}"
620
+ ${includeA11y ? 'aria-label="Button action"' : ""}
621
+ >
622
+ Click Me
623
+ </button>
624
+
625
+ <style>
626
+ .btn {
627
+ padding: 0.5rem 1rem;
628
+ border: none;
629
+ border-radius: 0.375rem;
630
+ font-weight: 500;
631
+ cursor: pointer;
632
+ transition: all 0.2s;
633
+ }
634
+
635
+ .btn:hover {
636
+ transform: translateY(-1px);
637
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
638
+ }
639
+
640
+ .btn:focus-visible {
641
+ outline: 2px solid #3b82f6;
642
+ outline-offset: 2px;
643
+ }
644
+
645
+ .btn-primary {
646
+ background: #3b82f6;
647
+ color: white;
648
+ }
649
+
650
+ .btn-primary:hover {
651
+ background: #2563eb;
652
+ }
653
+ </style>
654
+
655
+ <!-- UX Notes:
656
+ - Minimum 44x44px touch target
657
+ - Visible focus indicator (WCAG 2.4.7)
658
+ - Clear hover state
659
+ - Disabled state should use :disabled + opacity
660
+ -->`;
661
+ break;
662
+ case "modal":
663
+ case "dialog":
664
+ example = `<!-- Accessible Modal Dialog -->
665
+ <div
666
+ class="modal-backdrop"
667
+ ${includeA11y ? 'role="dialog" aria-modal="true" aria-labelledby="modal-title"' : ""}
668
+ >
669
+ <div class="modal-content">
670
+ <div class="modal-header">
671
+ <h2 ${includeA11y ? 'id="modal-title"' : ""}>Modal Title</h2>
672
+ <button
673
+ type="button"
674
+ class="modal-close"
675
+ ${includeA11y ? 'aria-label="Close modal"' : ""}
676
+ >
677
+ ×
678
+ </button>
679
+ </div>
680
+ <div class="modal-body">
681
+ <p>Modal content goes here.</p>
682
+ </div>
683
+ <div class="modal-footer">
684
+ <button type="button" class="btn btn-secondary">Cancel</button>
685
+ <button type="button" class="btn btn-primary">Confirm</button>
686
+ </div>
687
+ </div>
688
+ </div>
689
+
690
+ <style>
691
+ .modal-backdrop {
692
+ position: fixed;
693
+ inset: 0;
694
+ background: rgba(0, 0, 0, 0.5);
695
+ display: flex;
696
+ align-items: center;
697
+ justify-content: center;
698
+ z-index: 1000;
699
+ }
700
+
701
+ .modal-content {
702
+ background: white;
703
+ border-radius: 0.5rem;
704
+ max-width: 32rem;
705
+ width: 90%;
706
+ max-height: 90vh;
707
+ overflow-y: auto;
708
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
709
+ }
710
+
711
+ .modal-header {
712
+ display: flex;
713
+ justify-content: space-between;
714
+ align-items: center;
715
+ padding: 1rem;
716
+ border-bottom: 1px solid #e5e7eb;
717
+ }
718
+
719
+ .modal-body {
720
+ padding: 1rem;
721
+ }
722
+
723
+ .modal-footer {
724
+ display: flex;
725
+ justify-content: flex-end;
726
+ gap: 0.5rem;
727
+ padding: 1rem;
728
+ border-top: 1px solid #e5e7eb;
729
+ }
730
+ </style>
731
+
732
+ <script>
733
+ // JavaScript requirements:
734
+ // - Trap focus inside modal
735
+ // - Close on ESC key
736
+ // - Restore focus on close
737
+ // - Prevent body scroll when open
738
+ </script>
739
+
740
+ <!-- UX Notes:
741
+ - Focus trap implemented (WCAG 2.4.3)
742
+ - ESC key closes modal (WCAG 2.1.1)
743
+ - Click outside to close (optional)
744
+ - Backdrop prevents interaction with content behind
745
+ -->`;
746
+ break;
747
+ case "form":
748
+ example = `<!-- Accessible Form with Validation -->
749
+ <form class="form" ${includeA11y ? 'novalidate' : ""}>
750
+ <div class="form-field">
751
+ <label for="email" class="form-label">
752
+ Email Address ${includeA11y ? '<span aria-label="required">*</span>' : "*"}
753
+ </label>
754
+ <input
755
+ type="email"
756
+ id="email"
757
+ name="email"
758
+ class="form-input"
759
+ ${includeA11y ? 'aria-required="true" aria-describedby="email-error"' : ""}
760
+ placeholder="you@example.com"
761
+ />
762
+ <div
763
+ id="email-error"
764
+ class="form-error"
765
+ ${includeA11y ? 'role="alert" aria-live="polite"' : ""}
766
+ hidden
767
+ >
768
+ Please enter a valid email address
769
+ </div>
770
+ </div>
771
+
772
+ <div class="form-field">
773
+ <label for="password" class="form-label">
774
+ Password *
775
+ </label>
776
+ <input
777
+ type="password"
778
+ id="password"
779
+ name="password"
780
+ class="form-input"
781
+ ${includeA11y ? 'aria-required="true" aria-describedby="password-help password-error"' : ""}
782
+ />
783
+ <div id="password-help" class="form-help">
784
+ Must be at least 8 characters
785
+ </div>
786
+ <div id="password-error" class="form-error" hidden>
787
+ Password is required
788
+ </div>
789
+ </div>
790
+
791
+ <button type="submit" class="btn btn-primary">
792
+ Sign In
793
+ </button>
794
+ </form>
795
+
796
+ <style>
797
+ .form {
798
+ max-width: 24rem;
799
+ }
800
+
801
+ .form-field {
802
+ margin-bottom: 1rem;
803
+ }
804
+
805
+ .form-label {
806
+ display: block;
807
+ margin-bottom: 0.25rem;
808
+ font-weight: 500;
809
+ }
810
+
811
+ .form-input {
812
+ width: 100%;
813
+ padding: 0.5rem;
814
+ border: 1px solid #d1d5db;
815
+ border-radius: 0.375rem;
816
+ }
817
+
818
+ .form-input:focus {
819
+ outline: 2px solid #3b82f6;
820
+ outline-offset: 0;
821
+ border-color: #3b82f6;
822
+ }
823
+
824
+ .form-input[aria-invalid="true"] {
825
+ border-color: #ef4444;
826
+ }
827
+
828
+ .form-error {
829
+ color: #ef4444;
830
+ font-size: 0.875rem;
831
+ margin-top: 0.25rem;
832
+ }
833
+
834
+ .form-help {
835
+ color: #6b7280;
836
+ font-size: 0.875rem;
837
+ margin-top: 0.25rem;
838
+ }
839
+ </style>
840
+
841
+ <!-- UX Notes:
842
+ - Labels above inputs (WCAG 3.3.2)
843
+ - Validate on blur, show success on input
844
+ - Specific error messages (WCAG 3.3.1)
845
+ - aria-invalid toggles on validation
846
+ - Helper text shows requirements upfront
847
+ - Single column layout for better completion rate
848
+ -->`;
849
+ break;
850
+ case "search":
851
+ example = `<!-- Accessible Search Input -->
852
+ <form role="search" class="search-form">
853
+ <div class="search-container">
854
+ <label for="search" class="sr-only">Search</label>
855
+ <input
856
+ type="search"
857
+ id="search"
858
+ name="q"
859
+ class="search-input"
860
+ placeholder="Search..."
861
+ ${includeA11y ? 'aria-label="Search"' : ""}
862
+ autocomplete="off"
863
+ />
864
+ <button
865
+ type="submit"
866
+ class="search-button"
867
+ ${includeA11y ? 'aria-label="Submit search"' : ""}
868
+ >
869
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
870
+ <path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
871
+ </svg>
872
+ </button>
873
+ </div>
874
+ </form>
875
+
876
+ <style>
877
+ .search-form {
878
+ width: 100%;
879
+ max-width: 32rem;
880
+ }
881
+
882
+ .search-container {
883
+ position: relative;
884
+ display: flex;
885
+ align-items: center;
886
+ }
887
+
888
+ .search-input {
889
+ width: 100%;
890
+ padding: 0.5rem 2.5rem 0.5rem 1rem;
891
+ border: 1px solid #d1d5db;
892
+ border-radius: 0.5rem;
893
+ font-size: 1rem;
894
+ }
895
+
896
+ .search-input:focus {
897
+ outline: 2px solid #3b82f6;
898
+ outline-offset: 0;
899
+ border-color: #3b82f6;
900
+ }
901
+
902
+ .search-button {
903
+ position: absolute;
904
+ right: 0.5rem;
905
+ padding: 0.25rem;
906
+ background: none;
907
+ border: none;
908
+ cursor: pointer;
909
+ color: #6b7280;
910
+ }
911
+
912
+ .search-button:hover {
913
+ color: #111827;
914
+ }
915
+
916
+ .sr-only {
917
+ position: absolute;
918
+ width: 1px;
919
+ height: 1px;
920
+ padding: 0;
921
+ margin: -1px;
922
+ overflow: hidden;
923
+ clip: rect(0, 0, 0, 0);
924
+ white-space: nowrap;
925
+ border-width: 0;
926
+ }
927
+ </style>
928
+
929
+ <!-- UX Notes:
930
+ - role="search" for landmark navigation
931
+ - Label (can be visually hidden)
932
+ - Clear icon to reset search (add with JS)
933
+ - Search icon button is labeled
934
+ - Consider adding autocomplete/suggestions
935
+ -->`;
936
+ break;
937
+ default:
938
+ example = `Pattern "${pattern}" example not available.
939
+
940
+ Available patterns:
941
+ - button
942
+ - modal/dialog
943
+ - form
944
+ - search
945
+
946
+ Use suggest_pattern tool to find appropriate pattern for your use case.`;
947
+ }
948
+ return {
949
+ content: [
950
+ {
951
+ type: "text",
952
+ text: example,
953
+ },
954
+ ],
955
+ };
956
+ }
957
+ async function auditDesignSystem(args) {
958
+ const tokens = args.tokens;
959
+ const type = args.type || "all";
960
+ const issues = [];
961
+ const recommendations = [];
962
+ try {
963
+ // Try to parse as JSON
964
+ const parsed = JSON.parse(tokens);
965
+ // Check for common design token issues
966
+ if (type === "all" || type === "colors") {
967
+ if (parsed.colors || parsed.color) {
968
+ // Check for semantic naming
969
+ const colorKeys = Object.keys(parsed.colors || parsed.color || {});
970
+ const hasSemanticNames = colorKeys.some((k) => /^(primary|secondary|success|error|warning|info)/.test(k));
971
+ if (!hasSemanticNames) {
972
+ issues.push("⚠️ Colors lack semantic naming (primary, secondary, etc.)");
973
+ recommendations.push("Use semantic color names instead of generic names like 'blue' or 'red'");
974
+ }
975
+ }
976
+ }
977
+ if (type === "all" || type === "spacing") {
978
+ if (parsed.spacing || parsed.space) {
979
+ // Check for consistent scale
980
+ recommendations.push("✓ Spacing tokens found - verify they follow a consistent scale");
981
+ }
982
+ else {
983
+ issues.push("⚠️ No spacing tokens defined");
984
+ recommendations.push("Define spacing scale (base 4px or 8px)");
985
+ }
986
+ }
987
+ if (type === "all" || type === "typography") {
988
+ if (parsed.typography || parsed.fonts || parsed.fontSize) {
989
+ recommendations.push("✓ Typography tokens found");
990
+ }
991
+ else {
992
+ issues.push("⚠️ No typography tokens defined");
993
+ recommendations.push("Define font sizes, weights, and line heights");
994
+ }
995
+ }
996
+ const result = {
997
+ audit_type: type,
998
+ issues,
999
+ recommendations,
1000
+ best_practices: [
1001
+ "Use three-tier token structure: primitive → semantic → component",
1002
+ "Follow naming conventions consistently",
1003
+ "Use scales for sizing (spacing, typography)",
1004
+ "Support light/dark mode variants",
1005
+ ],
1006
+ reference: "See ux://design-systems/tokens for complete guidelines",
1007
+ };
1008
+ return {
1009
+ content: [
1010
+ {
1011
+ type: "text",
1012
+ text: JSON.stringify(result, null, 2),
1013
+ },
1014
+ ],
1015
+ };
1016
+ }
1017
+ catch (error) {
1018
+ return {
1019
+ content: [
1020
+ {
1021
+ type: "text",
1022
+ text: JSON.stringify({
1023
+ error: "Failed to parse tokens as JSON",
1024
+ message: "Provide valid JSON format for design tokens",
1025
+ }),
1026
+ },
1027
+ ],
1028
+ };
1029
+ }
1030
+ }
1031
+ async function checkContrast(args) {
1032
+ const fg = args.foreground;
1033
+ const bg = args.background;
1034
+ const level = args.level || "AA";
1035
+ const largeText = args.large_text;
1036
+ // Simple color parsing (hex only for now)
1037
+ const parseHex = (hex) => {
1038
+ const clean = hex.replace("#", "");
1039
+ return [
1040
+ parseInt(clean.substring(0, 2), 16),
1041
+ parseInt(clean.substring(2, 4), 16),
1042
+ parseInt(clean.substring(4, 6), 16),
1043
+ ];
1044
+ };
1045
+ const relativeLuminance = (r, g, b) => {
1046
+ const [rs, gs, bs] = [r, g, b].map((c) => {
1047
+ const s = c / 255;
1048
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
1049
+ });
1050
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
1051
+ };
1052
+ try {
1053
+ const [r1, g1, b1] = parseHex(fg);
1054
+ const [r2, g2, b2] = parseHex(bg);
1055
+ const l1 = relativeLuminance(r1, g1, b1);
1056
+ const l2 = relativeLuminance(r2, g2, b2);
1057
+ const lighter = Math.max(l1, l2);
1058
+ const darker = Math.min(l1, l2);
1059
+ const ratio = (lighter + 0.05) / (darker + 0.05);
1060
+ // WCAG requirements
1061
+ const requirements = {
1062
+ AA: largeText ? 3.0 : 4.5,
1063
+ AAA: largeText ? 4.5 : 7.0,
1064
+ };
1065
+ const required = requirements[level];
1066
+ const passes = ratio >= required;
1067
+ const result = {
1068
+ foreground: fg,
1069
+ background: bg,
1070
+ contrast_ratio: ratio.toFixed(2) + ":1",
1071
+ wcag_level: level,
1072
+ large_text: largeText,
1073
+ required_ratio: required + ":1",
1074
+ passes: passes ? "✓ PASS" : "✗ FAIL",
1075
+ recommendation: passes
1076
+ ? "Contrast ratio meets WCAG requirements"
1077
+ : `Increase contrast. Need at least ${required}:1, currently ${ratio.toFixed(2)}:1`,
1078
+ wcag_reference: "WCAG 1.4.3 (Contrast Minimum) and 1.4.6 (Contrast Enhanced)",
1079
+ };
1080
+ return {
1081
+ content: [
1082
+ {
1083
+ type: "text",
1084
+ text: JSON.stringify(result, null, 2),
1085
+ },
1086
+ ],
1087
+ };
1088
+ }
1089
+ catch (error) {
1090
+ return {
1091
+ content: [
1092
+ {
1093
+ type: "text",
1094
+ text: JSON.stringify({
1095
+ error: "Failed to parse colors",
1096
+ message: "Use hex color format (e.g., #3b82f6)",
1097
+ }),
1098
+ },
1099
+ ],
1100
+ };
1101
+ }
1102
+ }
1103
+ async function checkResponsive(args) {
1104
+ const code = args.code;
1105
+ const checkType = args.check_type || "all";
1106
+ const issues = [];
1107
+ const recommendations = [];
1108
+ // Check viewport meta tag
1109
+ if (checkType === "all" || checkType === "viewport") {
1110
+ if (/<html/i.test(code)) {
1111
+ const hasViewport = /<meta[^>]*name=["']viewport["']/i.test(code);
1112
+ if (!hasViewport) {
1113
+ issues.push("❌ Missing viewport meta tag");
1114
+ recommendations.push("Add: <meta name='viewport' content='width=device-width, initial-scale=1'>");
1115
+ }
1116
+ else {
1117
+ const hasUserScalableNo = /user-scalable\s*=\s*["']?no["']?/i.test(code);
1118
+ const hasMaxScale = /maximum-scale\s*=\s*["']?1["']?/i.test(code);
1119
+ if (hasUserScalableNo || hasMaxScale) {
1120
+ issues.push("⚠️ Viewport prevents zoom (accessibility issue)");
1121
+ recommendations.push("Remove user-scalable=no and maximum-scale=1 to allow users to zoom");
1122
+ }
1123
+ }
1124
+ }
1125
+ }
1126
+ // Check for mobile-first CSS
1127
+ if (checkType === "all" || checkType === "breakpoints") {
1128
+ const maxWidthQueries = (code.match(/@media[^{]*max-width/gi) || []).length;
1129
+ const minWidthQueries = (code.match(/@media[^{]*min-width/gi) || []).length;
1130
+ if (maxWidthQueries > minWidthQueries) {
1131
+ issues.push("⚠️ Desktop-first approach detected (more max-width than min-width queries)");
1132
+ recommendations.push("Consider mobile-first approach: base styles for mobile, min-width queries for larger screens");
1133
+ }
1134
+ }
1135
+ // Check touch targets
1136
+ if (checkType === "all" || checkType === "touch-targets") {
1137
+ if (/<button/i.test(code) || /<a/i.test(code)) {
1138
+ recommendations.push("Ensure interactive elements are at least 44x44px (iOS) or 48x48px (Android)");
1139
+ }
1140
+ }
1141
+ // Check responsive images
1142
+ if (checkType === "all" || checkType === "images") {
1143
+ const hasImg = /<img/i.test(code);
1144
+ if (hasImg) {
1145
+ const hasSrcset = /srcset=/i.test(code);
1146
+ const hasPicture = /<picture/i.test(code);
1147
+ if (!hasSrcset && !hasPicture) {
1148
+ issues.push("⚠️ Images without responsive sizing (srcset or picture element)");
1149
+ recommendations.push("Use srcset attribute or picture element for responsive images");
1150
+ }
1151
+ const hasLazyLoading = /loading=["']lazy["']/i.test(code);
1152
+ if (!hasLazyLoading) {
1153
+ recommendations.push("Consider adding loading='lazy' to below-the-fold images");
1154
+ }
1155
+ }
1156
+ }
1157
+ const result = {
1158
+ check_type: checkType,
1159
+ issues,
1160
+ recommendations,
1161
+ best_practices: [
1162
+ "Design for mobile first, then enhance for larger screens",
1163
+ "Use relative units (rem, em) instead of pixels",
1164
+ "Test on real devices, not just browser resize",
1165
+ "Minimum 44x44px touch targets",
1166
+ "Use responsive images with srcset",
1167
+ "Support landscape orientation"
1168
+ ],
1169
+ reference: "See ux://responsive/design for complete guidelines"
1170
+ };
1171
+ return {
1172
+ content: [
1173
+ {
1174
+ type: "text",
1175
+ text: JSON.stringify(result, null, 2),
1176
+ },
1177
+ ],
1178
+ };
1179
+ }
1180
+ async function suggestErrorMessage(args) {
1181
+ const scenario = args.scenario.toLowerCase();
1182
+ const context = args.context;
1183
+ const technicalMessage = args.technical_message;
1184
+ const errorMessages = await loadKnowledge("error-messages.json");
1185
+ let suggestion = {
1186
+ scenario,
1187
+ message: "",
1188
+ action: "",
1189
+ tone_guidance: []
1190
+ };
1191
+ // Match scenario to error message library
1192
+ if (scenario.includes("email") || scenario.includes("invalid") && scenario.includes("format")) {
1193
+ suggestion = {
1194
+ scenario: "Invalid email format",
1195
+ message: "Please enter a valid email address (e.g., name@example.com)",
1196
+ action: "Check your email format and try again",
1197
+ accessibility: [
1198
+ "Use aria-invalid='true' on input",
1199
+ "Link error with aria-describedby",
1200
+ "Display error with role='alert'"
1201
+ ],
1202
+ visual_placement: "Below the email input field",
1203
+ tone_guidance: ["Be helpful, not judgmental", "Provide example format", "Keep it concise"]
1204
+ };
1205
+ }
1206
+ else if (scenario.includes("required")) {
1207
+ suggestion = {
1208
+ scenario: "Required field empty",
1209
+ message: "Please enter [field name]",
1210
+ action: "Fill in the required information",
1211
+ accessibility: [
1212
+ "Mark with aria-required='true'",
1213
+ "Include asterisk (*) with aria-label='required'",
1214
+ "Announce error to screen readers"
1215
+ ],
1216
+ visual: "Red border + error icon + text message",
1217
+ tone_guidance: ["Be clear about what's needed", "Use 'please' to be polite"]
1218
+ };
1219
+ }
1220
+ else if (scenario.includes("password")) {
1221
+ suggestion = {
1222
+ scenario: "Weak password",
1223
+ message: "Password must include at least one uppercase letter, one number, and one special character",
1224
+ action: "Create a stronger password",
1225
+ progressive: "Show requirements checklist that updates as user types",
1226
+ accessibility: ["Announce each requirement met", "Use aria-live for updates"],
1227
+ tone_guidance: ["Be encouraging, not critical", "Show progress", "Explain why (security)"]
1228
+ };
1229
+ }
1230
+ else if (scenario.includes("payment") || scenario.includes("declined")) {
1231
+ suggestion = {
1232
+ scenario: "Payment failed",
1233
+ message: "Your payment couldn't be processed.",
1234
+ reasons: [
1235
+ "The card details might be incorrect",
1236
+ "There might be insufficient funds",
1237
+ "Your bank might be blocking the transaction"
1238
+ ],
1239
+ action: "Please check your payment details or try a different payment method",
1240
+ support: "Contact your bank if the problem persists",
1241
+ tone_guidance: ["Don't blame the user", "Provide possible reasons", "Offer alternatives", "Be reassuring"]
1242
+ };
1243
+ }
1244
+ else if (scenario.includes("file") && (scenario.includes("large") || scenario.includes("size"))) {
1245
+ suggestion = {
1246
+ scenario: "File too large",
1247
+ message: "This file is too large. Maximum file size is X MB.",
1248
+ action: "Please choose a smaller file or compress your image",
1249
+ helpful: "Show current file size and limit",
1250
+ tone_guidance: ["Be specific about the limit", "Suggest solutions", "Don't lose other form data"]
1251
+ };
1252
+ }
1253
+ else if (scenario.includes("offline") || scenario.includes("network") || scenario.includes("connection")) {
1254
+ suggestion = {
1255
+ scenario: "Network error",
1256
+ message: "You appear to be offline. Please check your internet connection and try again.",
1257
+ action: "Automatically retry when connection restored",
1258
+ visual: "Show connection status indicator",
1259
+ tone_guidance: ["Explain the problem clearly", "Provide reassurance", "Auto-retry when possible"]
1260
+ };
1261
+ }
1262
+ else {
1263
+ suggestion = {
1264
+ scenario: scenario,
1265
+ message: "Please provide more specific scenario",
1266
+ available_categories: Object.keys(errorMessages.categories),
1267
+ tip: "Try scenarios like: 'invalid email', 'required field', 'password weak', 'payment failed', 'file too large', 'network error'",
1268
+ reference: "See ux://content/error-messages for complete library"
1269
+ };
1270
+ }
1271
+ if (technicalMessage) {
1272
+ suggestion.technical_message = technicalMessage;
1273
+ suggestion.translation_tip = "Avoid exposing technical details to users. Translate to friendly language.";
1274
+ }
1275
+ if (context) {
1276
+ suggestion.context = context;
1277
+ }
1278
+ suggestion.general_principles = errorMessages.principles.good_error_messages;
1279
+ return {
1280
+ content: [
1281
+ {
1282
+ type: "text",
1283
+ text: JSON.stringify(suggestion, null, 2),
1284
+ },
1285
+ ],
1286
+ };
1287
+ }
1288
+ async function analyzePerformance(args) {
1289
+ const code = args.code;
1290
+ const checkType = args.check_type || "all";
1291
+ const issues = [];
1292
+ const recommendations = [];
1293
+ // Check images
1294
+ if (checkType === "all" || checkType === "images") {
1295
+ if (/<img/i.test(code)) {
1296
+ const hasWidthHeight = /<img[^>]*(width|height)=/i.test(code);
1297
+ if (!hasWidthHeight) {
1298
+ issues.push("⚠️ Images without width/height attributes (causes CLS)");
1299
+ recommendations.push("Add width and height to prevent Cumulative Layout Shift");
1300
+ }
1301
+ const hasLazyLoading = /loading=["']lazy["']/i.test(code);
1302
+ if (!hasLazyLoading) {
1303
+ recommendations.push("Consider adding loading='lazy' to below-fold images");
1304
+ }
1305
+ const hasModernFormat = /(\.webp|\.avif)/i.test(code);
1306
+ if (!hasModernFormat) {
1307
+ recommendations.push("Use modern image formats (WebP, AVIF) for better compression");
1308
+ }
1309
+ const hasSrcset = /srcset=/i.test(code);
1310
+ if (!hasSrcset) {
1311
+ recommendations.push("Use srcset for responsive images");
1312
+ }
1313
+ }
1314
+ }
1315
+ // Check CSS
1316
+ if (checkType === "all" || checkType === "css") {
1317
+ const hasBlockingCSS = /<link[^>]*rel=["']stylesheet["'][^>]*>/i.test(code);
1318
+ if (hasBlockingCSS) {
1319
+ recommendations.push("Consider inlining critical CSS and deferring non-critical styles");
1320
+ }
1321
+ }
1322
+ // Check JavaScript
1323
+ if (checkType === "all" || checkType === "javascript") {
1324
+ const hasBlockingJS = /<script(?![^>]*defer)(?![^>]*async)[^>]*src=/i.test(code);
1325
+ if (hasBlockingJS) {
1326
+ issues.push("⚠️ Render-blocking JavaScript detected");
1327
+ recommendations.push("Add defer or async attribute to <script> tags");
1328
+ }
1329
+ }
1330
+ // Check resource loading
1331
+ if (checkType === "all" || checkType === "loading") {
1332
+ const hasPreconnect = /<link[^>]*rel=["']preconnect["']/i.test(code);
1333
+ const hasThirdParty = /(fonts\.googleapis|cdnjs|cdn\.)/i.test(code);
1334
+ if (hasThirdParty && !hasPreconnect) {
1335
+ recommendations.push("Add <link rel='preconnect'> for third-party resources to improve LCP");
1336
+ }
1337
+ }
1338
+ const result = {
1339
+ check_type: checkType,
1340
+ issues,
1341
+ recommendations,
1342
+ core_web_vitals: {
1343
+ LCP: "Target: ≤ 2.5s - Optimize images, preload critical resources",
1344
+ INP: "Target: ≤ 200ms - Minimize JavaScript, use code splitting",
1345
+ CLS: "Target: ≤ 0.1 - Add dimensions to images/embeds, avoid content shifts"
1346
+ },
1347
+ quick_wins: [
1348
+ "Add width/height to images",
1349
+ "Use defer/async on scripts",
1350
+ "Enable gzip/brotli compression",
1351
+ "Lazy load below-fold images",
1352
+ "Use modern image formats (WebP, AVIF)"
1353
+ ],
1354
+ reference: "See ux://performance/optimization for complete guidelines"
1355
+ };
1356
+ return {
1357
+ content: [
1358
+ {
1359
+ type: "text",
1360
+ text: JSON.stringify(result, null, 2),
1361
+ },
1362
+ ],
1363
+ };
1364
+ }
1365
+ async function checkSEO(args) {
1366
+ const html = args.html;
1367
+ const url = args.url;
1368
+ const issues = [];
1369
+ const recommendations = [];
1370
+ const found = [];
1371
+ // Check title
1372
+ const titleMatch = /<title[^>]*>([^<]+)<\/title>/i.exec(html);
1373
+ if (!titleMatch) {
1374
+ issues.push("❌ Missing <title> tag");
1375
+ }
1376
+ else {
1377
+ const titleLength = titleMatch[1].length;
1378
+ found.push(`Title: "${titleMatch[1]}" (${titleLength} chars)`);
1379
+ if (titleLength < 30) {
1380
+ issues.push("⚠️ Title too short (< 30 characters)");
1381
+ }
1382
+ else if (titleLength > 60) {
1383
+ issues.push("⚠️ Title too long (> 60 characters, may be truncated)");
1384
+ }
1385
+ }
1386
+ // Check meta description
1387
+ const descMatch = /<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["']/i.exec(html);
1388
+ if (!descMatch) {
1389
+ issues.push("❌ Missing meta description");
1390
+ }
1391
+ else {
1392
+ const descLength = descMatch[1].length;
1393
+ found.push(`Meta description: ${descLength} chars`);
1394
+ if (descLength < 120) {
1395
+ issues.push("⚠️ Meta description too short (< 120 characters)");
1396
+ }
1397
+ else if (descLength > 160) {
1398
+ issues.push("⚠️ Meta description too long (> 160 characters)");
1399
+ }
1400
+ }
1401
+ // Check canonical
1402
+ if (/<link[^>]*rel=["']canonical["']/i.test(html)) {
1403
+ found.push("Canonical tag present");
1404
+ }
1405
+ else {
1406
+ recommendations.push("Add canonical tag to prevent duplicate content issues");
1407
+ }
1408
+ // Check Open Graph
1409
+ const hasOG = /<meta[^>]*property=["']og:/i.test(html);
1410
+ if (hasOG) {
1411
+ found.push("Open Graph tags present");
1412
+ }
1413
+ else {
1414
+ recommendations.push("Add Open Graph tags for better social media sharing");
1415
+ }
1416
+ // Check Twitter Cards
1417
+ if (/<meta[^>]*name=["']twitter:card["']/i.test(html)) {
1418
+ found.push("Twitter Card tags present");
1419
+ }
1420
+ else {
1421
+ recommendations.push("Add Twitter Card tags for better Twitter sharing");
1422
+ }
1423
+ // Check viewport
1424
+ if (!/<meta[^>]*name=["']viewport["']/i.test(html)) {
1425
+ issues.push("❌ Missing viewport meta tag (affects mobile SEO)");
1426
+ }
1427
+ // Check structured data
1428
+ if (/<script[^>]*type=["']application\/ld\+json["']/i.test(html)) {
1429
+ found.push("JSON-LD structured data present");
1430
+ }
1431
+ else {
1432
+ recommendations.push("Add structured data (JSON-LD) for rich snippets");
1433
+ }
1434
+ // Check headings
1435
+ const h1Count = (html.match(/<h1[^>]*>/gi) || []).length;
1436
+ if (h1Count === 0) {
1437
+ issues.push("❌ No H1 heading found");
1438
+ }
1439
+ else if (h1Count > 1) {
1440
+ issues.push(`⚠️ Multiple H1 headings (${h1Count}) - use only one per page`);
1441
+ }
1442
+ else {
1443
+ found.push("Single H1 heading (good)");
1444
+ }
1445
+ // Check alt text
1446
+ const imgCount = (html.match(/<img/gi) || []).length;
1447
+ const altCount = (html.match(/<img[^>]*alt=/gi) || []).length;
1448
+ if (imgCount > 0 && altCount < imgCount) {
1449
+ issues.push(`⚠️ ${imgCount - altCount} image(s) missing alt text`);
1450
+ }
1451
+ const result = {
1452
+ url: url || "Not specified",
1453
+ found,
1454
+ issues,
1455
+ recommendations,
1456
+ seo_checklist: [
1457
+ "Title: 50-60 characters with primary keyword",
1458
+ "Meta description: 120-160 characters",
1459
+ "Canonical tag for duplicate content",
1460
+ "Open Graph tags for social sharing",
1461
+ "Structured data (JSON-LD) for rich snippets",
1462
+ "Single H1 per page",
1463
+ "Alt text on all images",
1464
+ "Mobile-friendly (viewport meta tag)"
1465
+ ],
1466
+ reference: "See ux://seo/guidelines for complete guide"
1467
+ };
1468
+ return {
1469
+ content: [
1470
+ {
1471
+ type: "text",
1472
+ text: JSON.stringify(result, null, 2),
1473
+ },
1474
+ ],
1475
+ };
1476
+ }
1477
+ async function suggestAnimation(args) {
1478
+ const interaction = args.interaction.toLowerCase();
1479
+ const context = args.context;
1480
+ let suggestion = {
1481
+ interaction,
1482
+ context: context || "Not specified"
1483
+ };
1484
+ // Match interaction to animation patterns
1485
+ if (interaction.includes("button") && (interaction.includes("click") || interaction.includes("press"))) {
1486
+ suggestion = {
1487
+ ...suggestion,
1488
+ animation_type: "Micro-interaction",
1489
+ sequence: [
1490
+ "Scale down to 0.95 on press",
1491
+ "Scale back to 1.0 on release",
1492
+ "Optional: Ripple effect from click point"
1493
+ ],
1494
+ duration: "100-150ms total",
1495
+ easing: "ease-out",
1496
+ css_example: ".button {\n transition: transform 100ms ease-out;\n}\n.button:active {\n transform: scale(0.95);\n}",
1497
+ accessibility: "Respect prefers-reduced-motion"
1498
+ };
1499
+ }
1500
+ else if (interaction.includes("modal") || interaction.includes("dialog")) {
1501
+ const isOpen = interaction.includes("open") || interaction.includes("show");
1502
+ suggestion = {
1503
+ ...suggestion,
1504
+ animation_type: "Modal transition",
1505
+ backdrop: "Fade in from transparent to semi-opaque (200ms)",
1506
+ content: isOpen ? "Scale from 0.9 to 1.0 + fade in (250-300ms)" : "Scale to 0.9 + fade out (200ms)",
1507
+ timing: isOpen ? "Backdrop first, then content with 100ms delay" : "Content first, then backdrop",
1508
+ duration: isOpen ? "300ms enter" : "200ms exit",
1509
+ easing: "ease-out",
1510
+ css_example: "@keyframes modalEnter {\n from { opacity: 0; transform: scale(0.9); }\n to { opacity: 1; transform: scale(1); }\n}\n\n.modal {\n animation: modalEnter 300ms ease-out;\n}"
1511
+ };
1512
+ }
1513
+ else if (interaction.includes("list") || interaction.includes("item")) {
1514
+ suggestion = {
1515
+ ...suggestion,
1516
+ animation_type: "List animation",
1517
+ pattern: "Staggered entrance",
1518
+ timing: "50-100ms delay between items",
1519
+ duration: "200-250ms per item",
1520
+ easing: "ease-out",
1521
+ limit: "Animate first 10-15 items only (rest appear instantly)",
1522
+ css_example: ".list-item {\n opacity: 0;\n transform: translateY(20px);\n animation: itemEnter 250ms ease-out forwards;\n}\n\n.list-item:nth-child(1) { animation-delay: 0ms; }\n.list-item:nth-child(2) { animation-delay: 50ms; }\n.list-item:nth-child(3) { animation-delay: 100ms; }\n\n@keyframes itemEnter {\n to { opacity: 1; transform: translateY(0); }\n}"
1523
+ };
1524
+ }
1525
+ else if (interaction.includes("page") || interaction.includes("route") || interaction.includes("transition")) {
1526
+ suggestion = {
1527
+ ...suggestion,
1528
+ animation_type: "Page transition",
1529
+ options: [
1530
+ {
1531
+ type: "Fade",
1532
+ use: "Unrelated pages",
1533
+ duration: "200-300ms",
1534
+ implementation: "Cross-fade old and new content"
1535
+ },
1536
+ {
1537
+ type: "Slide",
1538
+ use: "Forward/back navigation",
1539
+ duration: "300-400ms",
1540
+ direction: "Left for forward, right for back"
1541
+ }
1542
+ ],
1543
+ recommendation: "Use fade for simplicity, slide for directional navigation",
1544
+ duration: "300-400ms",
1545
+ easing: "ease-in-out"
1546
+ };
1547
+ }
1548
+ else if (interaction.includes("toast") || interaction.includes("notification")) {
1549
+ suggestion = {
1550
+ ...suggestion,
1551
+ animation_type: "Toast notification",
1552
+ enter: "Slide in from top/bottom + fade (300ms)",
1553
+ exit: "Fade out + slight slide (200ms)",
1554
+ auto_dismiss: "After 4-6 seconds",
1555
+ easing: "ease-out for enter, ease-in for exit",
1556
+ placement: "Top-right or bottom-left typical",
1557
+ css_example: "@keyframes toastEnter {\n from {\n opacity: 0;\n transform: translateY(-100%);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.toast {\n animation: toastEnter 300ms ease-out;\n}"
1558
+ };
1559
+ }
1560
+ else if (interaction.includes("hover")) {
1561
+ suggestion = {
1562
+ ...suggestion,
1563
+ animation_type: "Hover state",
1564
+ effects: [
1565
+ "Subtle scale (1.0 → 1.05)",
1566
+ "Lift effect (add shadow, translate up slightly)",
1567
+ "Color transition",
1568
+ "Underline grow"
1569
+ ],
1570
+ duration: "150-200ms",
1571
+ easing: "ease-out",
1572
+ note: "Keep subtle, instant feedback is key",
1573
+ css_example: ".card {\n transition: transform 150ms ease-out, box-shadow 150ms ease-out;\n}\n\n.card:hover {\n transform: translateY(-2px);\n box-shadow: 0 10px 20px rgba(0,0,0,0.1);\n}"
1574
+ };
1575
+ }
1576
+ else {
1577
+ suggestion = {
1578
+ ...suggestion,
1579
+ message: "No specific match found for this interaction",
1580
+ general_guidelines: {
1581
+ duration: "200-300ms for most UI animations",
1582
+ easing: "ease-out for enter, ease-in for exit, ease-in-out for movement",
1583
+ properties: "Animate transform and opacity only (GPU accelerated)",
1584
+ accessibility: "Always respect prefers-reduced-motion"
1585
+ },
1586
+ reference: "See ux://animation/motion for complete animation library"
1587
+ };
1588
+ }
1589
+ suggestion.performance_tip = "Use transform and opacity only for best performance (60fps)";
1590
+ suggestion.accessibility_requirement = "Implement prefers-reduced-motion to disable/reduce animations";
1591
+ suggestion.reference = "See ux://animation/motion for complete motion design guide";
1592
+ return {
1593
+ content: [
1594
+ {
1595
+ type: "text",
1596
+ text: JSON.stringify(suggestion, null, 2),
1597
+ },
1598
+ ],
1599
+ };
1600
+ }
1601
+ // ========================================
1602
+ // PROMPTS - Pre-configured UX Reviews
1603
+ // ========================================
1604
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
1605
+ return {
1606
+ prompts: [
1607
+ {
1608
+ name: "accessibility_review",
1609
+ description: "Comprehensive accessibility review following WCAG 2.1 AA guidelines",
1610
+ arguments: [
1611
+ {
1612
+ name: "component",
1613
+ description: "Component name or description to review",
1614
+ required: true,
1615
+ },
1616
+ {
1617
+ name: "code",
1618
+ description: "Code to analyze (optional)",
1619
+ required: false,
1620
+ },
1621
+ ],
1622
+ },
1623
+ {
1624
+ name: "usability_audit",
1625
+ description: "Full usability audit using Nielsen's 10 heuristics",
1626
+ arguments: [
1627
+ {
1628
+ name: "interface",
1629
+ description: "Interface or feature to audit",
1630
+ required: true,
1631
+ },
1632
+ ],
1633
+ },
1634
+ {
1635
+ name: "design_system_setup",
1636
+ description: "Guide for setting up a new design system with tokens and components",
1637
+ arguments: [
1638
+ {
1639
+ name: "project_type",
1640
+ description: "Type of project (web app, mobile, etc.)",
1641
+ required: false,
1642
+ },
1643
+ ],
1644
+ },
1645
+ ],
1646
+ };
1647
+ });
1648
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
1649
+ const { name, arguments: args } = request.params;
1650
+ switch (name) {
1651
+ case "accessibility_review":
1652
+ return {
1653
+ messages: [
1654
+ {
1655
+ role: "user",
1656
+ content: {
1657
+ type: "text",
1658
+ text: `Please perform a comprehensive accessibility review for: ${args?.component}
1659
+
1660
+ ${args?.code ? `Code to analyze:\n\`\`\`\n${args.code}\n\`\`\`\n` : ""}
1661
+
1662
+ Review checklist:
1663
+ 1. Use the analyze_accessibility tool to check for WCAG violations
1664
+ 2. Check keyboard navigation (tab order, focus management, keyboard shortcuts)
1665
+ 3. Verify screen reader compatibility (ARIA labels, roles, live regions)
1666
+ 4. Test color contrast with check_contrast tool
1667
+ 5. Review semantic HTML structure
1668
+ 6. Check for proper heading hierarchy
1669
+ 7. Verify form labels and error handling
1670
+ 8. Test with keyboard only (no mouse)
1671
+
1672
+ Provide specific, actionable recommendations for each issue found.
1673
+
1674
+ Reference the WCAG guidelines resource: ux://accessibility/wcag for detailed standards.`,
1675
+ },
1676
+ },
1677
+ ],
1678
+ };
1679
+ case "usability_audit":
1680
+ return {
1681
+ messages: [
1682
+ {
1683
+ role: "user",
1684
+ content: {
1685
+ type: "text",
1686
+ text: `Please conduct a full usability audit for: ${args?.interface}
1687
+
1688
+ Evaluate against all 10 of Nielsen's Usability Heuristics:
1689
+
1690
+ 1. Visibility of System Status
1691
+ 2. Match Between System and Real World
1692
+ 3. User Control and Freedom
1693
+ 4. Consistency and Standards
1694
+ 5. Error Prevention
1695
+ 6. Recognition Rather than Recall
1696
+ 7. Flexibility and Efficiency of Use
1697
+ 8. Aesthetic and Minimalist Design
1698
+ 9. Help Users Recognize, Diagnose, and Recover from Errors
1699
+ 10. Help and Documentation
1700
+
1701
+ For each heuristic:
1702
+ - Rate severity (0-4): 0=no problem, 4=usability catastrophe
1703
+ - Describe specific violations
1704
+ - Provide actionable recommendations
1705
+
1706
+ Use the review_usability tool to get detailed heuristic descriptions.
1707
+
1708
+ Reference: ux://usability/nielsen-heuristics`,
1709
+ },
1710
+ },
1711
+ ],
1712
+ };
1713
+ case "design_system_setup":
1714
+ return {
1715
+ messages: [
1716
+ {
1717
+ role: "user",
1718
+ content: {
1719
+ type: "text",
1720
+ text: `Please help me set up a design system${args?.project_type ? ` for a ${args.project_type}` : ""}.
1721
+
1722
+ Guide me through:
1723
+
1724
+ 1. **Design Tokens Setup**
1725
+ - Color palette (primary, secondary, semantic colors)
1726
+ - Spacing scale (base unit: 4px or 8px)
1727
+ - Typography scale (font families, sizes, weights)
1728
+ - Border radius values
1729
+ - Shadow levels
1730
+ - Animation durations
1731
+
1732
+ 2. **Component Architecture**
1733
+ - Atomic design structure (atoms → molecules → organisms)
1734
+ - Component API design principles
1735
+ - Naming conventions
1736
+ - Composition patterns
1737
+
1738
+ 3. **Implementation Checklist**
1739
+ - File structure recommendation
1740
+ - Tools and libraries to consider
1741
+ - Documentation approach
1742
+ - Testing strategy
1743
+
1744
+ Reference the design system resource: ux://design-systems/tokens
1745
+
1746
+ Provide code examples for token definition and a starter component.`,
1747
+ },
1748
+ },
1749
+ ],
1750
+ };
1751
+ default:
1752
+ throw new Error(`Unknown prompt: ${name}`);
1753
+ }
1754
+ });
1755
+ // ========================================
1756
+ // START SERVER
1757
+ // ========================================
1758
+ async function main() {
1759
+ const transport = new StdioServerTransport();
1760
+ await server.connect(transport);
1761
+ console.error("UX MCP Server running on stdio");
1762
+ }
1763
+ main().catch((error) => {
1764
+ console.error("Fatal error in main():", error);
1765
+ process.exit(1);
1766
+ });