@primer/mcp 0.2.0 → 0.3.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.
@@ -0,0 +1,1444 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import * as cheerio from 'cheerio';
3
+ import * as z from 'zod';
4
+ import TurndownService from 'turndown';
5
+ import componentsMetadata from '@primer/react/generated/components.json' with { type: 'json' };
6
+ import octicons from '@primer/octicons/build/data.json' with { type: 'json' };
7
+ import { readFileSync } from 'node:fs';
8
+ import { createRequire } from 'node:module';
9
+ import baseMotion from '@primer/primitives/dist/docs/base/motion/motion.json' with { type: 'json' };
10
+ import baseSize from '@primer/primitives/dist/docs/base/size/size.json' with { type: 'json' };
11
+ import baseTypography from '@primer/primitives/dist/docs/base/typography/typography.json' with { type: 'json' };
12
+ import functionalSizeBorder from '@primer/primitives/dist/docs/functional/size/border.json' with { type: 'json' };
13
+ import functionalSizeCoarse from '@primer/primitives/dist/docs/functional/size/size-coarse.json' with { type: 'json' };
14
+ import functionalSizeFine from '@primer/primitives/dist/docs/functional/size/size-fine.json' with { type: 'json' };
15
+ import functionalSize from '@primer/primitives/dist/docs/functional/size/size.json' with { type: 'json' };
16
+ import light from '@primer/primitives/dist/docs/functional/themes/light.json' with { type: 'json' };
17
+ import functionalTypography from '@primer/primitives/dist/docs/functional/typography/typography.json' with { type: 'json' };
18
+
19
+ function idToSlug(id) {
20
+ if (id === 'actionbar') {
21
+ return 'action-bar';
22
+ }
23
+ if (id === 'tooltip-v2') {
24
+ return 'tooltip';
25
+ }
26
+ if (id === 'dialog_v2') {
27
+ return 'dialog';
28
+ }
29
+ return id.replaceAll('_', '-');
30
+ }
31
+ const components = Object.entries(componentsMetadata.components).map(([id, component]) => {
32
+ return {
33
+ id,
34
+ name: component.name,
35
+ importPath: component.importPath,
36
+ slug: idToSlug(id)
37
+ };
38
+ });
39
+ function listComponents() {
40
+ return components;
41
+ }
42
+ const patterns = [{
43
+ id: 'data-visualization',
44
+ name: 'Data Visualization'
45
+ }, {
46
+ id: 'degraded-experiences',
47
+ name: 'Degraded Experiences'
48
+ }, {
49
+ id: 'empty-states',
50
+ name: 'Empty States'
51
+ }, {
52
+ id: 'feature-onboarding',
53
+ name: 'Feature Onboarding'
54
+ }, {
55
+ id: 'forms',
56
+ name: 'Forms'
57
+ }, {
58
+ id: 'loading',
59
+ name: 'Loading'
60
+ }, {
61
+ id: 'navigation',
62
+ name: 'Navigation'
63
+ }, {
64
+ id: 'notification-messaging',
65
+ name: 'Notification message'
66
+ }, {
67
+ id: 'progressive-disclosure',
68
+ name: 'Progressive disclosure'
69
+ }, {
70
+ id: 'saving',
71
+ name: 'Saving'
72
+ }];
73
+ function listPatterns() {
74
+ return patterns;
75
+ }
76
+ const icons = Object.values(octicons).map(icon => {
77
+ return {
78
+ name: icon.name,
79
+ keywords: icon.keywords,
80
+ heights: Object.keys(icon.heights)
81
+ };
82
+ });
83
+ function listIcons() {
84
+ return icons;
85
+ }
86
+
87
+ // radius.json may not exist in all versions of @primer/primitives
88
+ let functionalSizeRadius = {};
89
+ try {
90
+ const require = createRequire(import.meta.url);
91
+ const radiusPath = require.resolve('@primer/primitives/dist/docs/functional/size/radius.json');
92
+ functionalSizeRadius = JSON.parse(readFileSync(radiusPath, 'utf-8'));
93
+ } catch {
94
+ // radius.json not available in this version of @primer/primitives
95
+ }
96
+ const categories = {
97
+ base: {
98
+ motion: Object.values(baseMotion).map(token => {
99
+ return {
100
+ name: token.name,
101
+ type: token.type,
102
+ value: token.value
103
+ };
104
+ }),
105
+ size: Object.values(baseSize).map(token => {
106
+ return {
107
+ name: token.name,
108
+ type: token.type,
109
+ value: token.value
110
+ };
111
+ }),
112
+ typography: Object.values(baseTypography).map(token => {
113
+ return {
114
+ name: token.name,
115
+ type: token.type,
116
+ value: token.value
117
+ };
118
+ })
119
+ },
120
+ functional: {
121
+ border: Object.values(functionalSizeBorder).map(token => {
122
+ return {
123
+ name: token.name,
124
+ type: token.type,
125
+ value: token.value
126
+ };
127
+ }),
128
+ radius: Object.values(functionalSizeRadius).map(token => {
129
+ return {
130
+ name: token.name,
131
+ type: token.type,
132
+ value: token.value
133
+ };
134
+ }),
135
+ sizeCoarse: Object.values(functionalSizeCoarse).map(token => {
136
+ return {
137
+ name: token.name,
138
+ type: token.type,
139
+ value: token.value
140
+ };
141
+ }),
142
+ sizeFine: Object.values(functionalSizeFine).map(token => {
143
+ return {
144
+ name: token.name,
145
+ type: token.type,
146
+ value: token.value
147
+ };
148
+ }),
149
+ size: Object.values(functionalSize).map(token => {
150
+ return {
151
+ name: token.name,
152
+ type: token.type,
153
+ value: token.value
154
+ };
155
+ }),
156
+ themes: {
157
+ light: Object.values(light).map(token => {
158
+ return {
159
+ name: token.name,
160
+ type: token.type,
161
+ value: token.value
162
+ };
163
+ })
164
+ },
165
+ typography: Object.values(functionalTypography).map(token => {
166
+ return {
167
+ name: token.name,
168
+ type: token.type,
169
+ value: token.value
170
+ };
171
+ })
172
+ }
173
+ };
174
+ const tokens = [...categories.base.motion, ...categories.base.size, ...categories.base.typography, ...categories.functional.border, ...categories.functional.radius, ...categories.functional.sizeCoarse, ...categories.functional.sizeFine, ...categories.functional.size, ...categories.functional.themes.light, ...categories.functional.typography];
175
+
176
+ // Semantic group prefixes that apply to any element
177
+ const SEMANTIC_PREFIXES = ['bgColor', 'fgColor', 'border', 'borderColor', 'shadow', 'focus', 'color'];
178
+ function listTokenGroups() {
179
+ // Use the full token set so non-theme groups (stack, text, borderRadius, etc.) are included
180
+ const allTokens = tokens;
181
+
182
+ // Group tokens by their first segment
183
+ const groupMap = new Map();
184
+ for (const token of allTokens) {
185
+ const parts = token.name.split('-');
186
+ const prefix = parts[0];
187
+ if (!groupMap.has(prefix)) {
188
+ groupMap.set(prefix, {
189
+ count: 0,
190
+ subGroups: new Set()
191
+ });
192
+ }
193
+ const group = groupMap.get(prefix);
194
+ group.count++;
195
+
196
+ // For component tokens, track sub-groups (e.g., button-bgColor -> bgColor)
197
+ if (!SEMANTIC_PREFIXES.includes(prefix) && parts.length > 1) {
198
+ const subGroup = parts[1];
199
+ if (SEMANTIC_PREFIXES.includes(subGroup)) {
200
+ group.subGroups.add(subGroup);
201
+ }
202
+ }
203
+ }
204
+ const semantic = [];
205
+ const component = [];
206
+ for (const [name, data] of groupMap.entries()) {
207
+ const group = {
208
+ name,
209
+ count: data.count
210
+ };
211
+ if (data.subGroups.size > 0) {
212
+ group.subGroups = Array.from(data.subGroups).sort();
213
+ }
214
+ if (SEMANTIC_PREFIXES.includes(name)) {
215
+ semantic.push(group);
216
+ } else {
217
+ component.push(group);
218
+ }
219
+ }
220
+
221
+ // Sort by count descending
222
+ semantic.sort((a, b) => b.count - a.count);
223
+ component.sort((a, b) => b.count - a.count);
224
+ return {
225
+ semantic,
226
+ component
227
+ };
228
+ }
229
+
230
+ // Token with guidelines from markdown
231
+
232
+ // Parse design tokens spec from new DESIGN_TOKENS_SPEC.md format
233
+ function parseDesignTokensSpec(markdown) {
234
+ const results = [];
235
+ const lines = markdown.split('\n');
236
+ let currentGroup = '';
237
+ let currentToken = null;
238
+ let descriptionLines = [];
239
+ for (const line of lines) {
240
+ // Match group headings (## heading)
241
+ const groupMatch = line.match(/^## (.+)$/);
242
+ if (groupMatch) {
243
+ // Save previous token if exists
244
+ if (currentToken?.name) {
245
+ results.push({
246
+ name: currentToken.name,
247
+ value: getTokenValue(currentToken.name),
248
+ useCase: currentToken.useCase || descriptionLines.join(' '),
249
+ rules: currentToken.rules || '',
250
+ group: currentToken.group || ''
251
+ });
252
+ descriptionLines = [];
253
+ }
254
+ currentGroup = groupMatch[1].trim();
255
+ currentToken = null;
256
+ continue;
257
+ }
258
+
259
+ // Match token name (### tokenName)
260
+ const tokenMatch = line.match(/^### (.+)$/);
261
+ if (tokenMatch) {
262
+ // Save previous token if exists
263
+ if (currentToken?.name) {
264
+ results.push({
265
+ name: currentToken.name,
266
+ value: getTokenValue(currentToken.name),
267
+ useCase: currentToken.useCase || descriptionLines.join(' '),
268
+ rules: currentToken.rules || '',
269
+ group: currentToken.group || ''
270
+ });
271
+ }
272
+ descriptionLines = [];
273
+ currentToken = {
274
+ name: tokenMatch[1].trim(),
275
+ group: currentGroup
276
+ };
277
+ continue;
278
+ }
279
+
280
+ // Match new format usage line (**U:**)
281
+ const newUsageMatch = line.match(/^\*\*U:\*\*\s*(.+)$/);
282
+ if (newUsageMatch && currentToken) {
283
+ currentToken.useCase = newUsageMatch[1].trim();
284
+ continue;
285
+ }
286
+
287
+ // Match new format rules line (**R:**)
288
+ const newRulesMatch = line.match(/^\*\*R:\*\*\s*(.+)$/);
289
+ if (newRulesMatch && currentToken) {
290
+ currentToken.rules = newRulesMatch[1].trim();
291
+ continue;
292
+ }
293
+
294
+ // Description line (line after token name, before U:/R:)
295
+ if (currentToken && !currentToken.useCase && !line.startsWith('**') && line.trim() && !line.startsWith('#')) {
296
+ descriptionLines.push(line.trim());
297
+ }
298
+ }
299
+
300
+ // Don't forget the last token
301
+ if (currentToken?.name) {
302
+ results.push({
303
+ name: currentToken.name,
304
+ value: getTokenValue(currentToken.name),
305
+ useCase: currentToken.useCase || descriptionLines.join(' '),
306
+ rules: currentToken.rules || '',
307
+ group: currentToken.group || ''
308
+ });
309
+ }
310
+ return results;
311
+ }
312
+
313
+ // Get token value from the loaded tokens
314
+ function getTokenValue(tokenName) {
315
+ const found = tokens.find(token => token.name === tokenName);
316
+ return found ? String(found.value) : '';
317
+ }
318
+
319
+ // Human-readable display labels for canonical group prefixes
320
+ const GROUP_LABELS = {
321
+ bgColor: 'Background Color',
322
+ fgColor: 'Foreground Color',
323
+ borderColor: 'Border Color',
324
+ border: 'Border',
325
+ shadow: 'Shadow',
326
+ focus: 'Focus',
327
+ color: 'Color',
328
+ borderWidth: 'Border Width',
329
+ borderRadius: 'Border Radius',
330
+ boxShadow: 'Box Shadow',
331
+ controlStack: 'Control Stack',
332
+ fontStack: 'Font Stack',
333
+ outline: 'Outline',
334
+ text: 'Text',
335
+ control: 'Control',
336
+ overlay: 'Overlay',
337
+ stack: 'Stack',
338
+ spinner: 'Spinner'
339
+ };
340
+
341
+ // Get canonical group prefix from token name
342
+ function getGroupFromName(name) {
343
+ return name.split('-')[0];
344
+ }
345
+
346
+ // Build complete token list from JSON (includes all tokens, not just those with guidelines)
347
+ function buildAllTokens(guidelinesTokens) {
348
+ const guidelinesMap = new Map(guidelinesTokens.map(t => [t.name, t]));
349
+
350
+ // Include theme tokens AND size/typography/border tokens
351
+ const allSourceTokens = [...categories.base.motion, ...categories.base.size, ...categories.base.typography, ...categories.functional.themes.light, ...categories.functional.size, ...categories.functional.sizeCoarse, ...categories.functional.sizeFine, ...categories.functional.border, ...categories.functional.radius, ...categories.functional.typography];
352
+ const allTokens = [];
353
+ const seen = new Set();
354
+ for (const token of allSourceTokens) {
355
+ if (seen.has(token.name)) continue;
356
+ seen.add(token.name);
357
+ const existing = guidelinesMap.get(token.name);
358
+ if (existing) {
359
+ allTokens.push(existing);
360
+ } else {
361
+ allTokens.push({
362
+ name: token.name,
363
+ value: String(token.value),
364
+ useCase: '',
365
+ rules: '',
366
+ group: getGroupFromName(token.name)
367
+ });
368
+ }
369
+ }
370
+ return allTokens;
371
+ }
372
+
373
+ // Expand token patterns like [accent, danger] into multiple tokens
374
+ function expandTokenPattern(token) {
375
+ const bracketRegex = /\[(.*?)\]/;
376
+ const match = token.name.match(bracketRegex);
377
+ if (!match) return [token];
378
+ const variants = match[1].split(',').map(s => s.trim());
379
+ return variants.map(variant => ({
380
+ ...token,
381
+ name: token.name.replace(bracketRegex, variant)
382
+ }));
383
+ }
384
+
385
+ // Load and parse token guidelines, then build complete token list
386
+ function loadAllTokensWithGuidelines() {
387
+ try {
388
+ const specMarkdown = loadDesignTokensSpec();
389
+ const specTokens = parseDesignTokensSpec(specMarkdown);
390
+ return buildAllTokens(specTokens);
391
+ } catch {
392
+ // DESIGN_TOKENS_SPEC.md not available in this version of @primer/primitives
393
+ return buildAllTokens([]);
394
+ }
395
+ }
396
+
397
+ // Load the design tokens guide (logic, rules, patterns, golden examples)
398
+ function loadDesignTokensGuide() {
399
+ const require = createRequire(import.meta.url);
400
+ const guidePath = require.resolve('@primer/primitives/DESIGN_TOKENS_GUIDE.md');
401
+ return readFileSync(guidePath, 'utf-8');
402
+ }
403
+
404
+ // Load the design tokens spec (token dictionary with use cases and rules)
405
+ function loadDesignTokensSpec() {
406
+ const require = createRequire(import.meta.url);
407
+ const specPath = require.resolve('@primer/primitives/DESIGN_TOKENS_SPEC.md');
408
+ return readFileSync(specPath, 'utf-8');
409
+ }
410
+
411
+ // Get design token specifications text with dynamic group information
412
+ function getDesignTokenSpecsText(groups) {
413
+ return `
414
+ # Design Token Specifications
415
+
416
+ ## 1. Core Rule & Enforcement
417
+ - **Expert Mode**: You are a CSS expert. NEVER use raw values (hex, px, etc.). Only use tokens.
418
+ - **Shorthand**: MUST use shorthand tokens (e.g., \`font: var(...)\`). NEVER split font-size/weight.
419
+ - **States**: MUST define 5 states: Rest, Hover, Focus-visible, Active, Disabled.
420
+ - **Safety**: If unsure of a token name, suffix with \`/* check-token */\`.
421
+ - **Focus States**: When implementing :focus-visible, you MUST use both:
422
+ - outline: var(--focus-outline)
423
+ - outline-offset: var(--outline-focus-offset)
424
+
425
+ ## 2. Typography Constraints (STRICT)
426
+ - **Body Only**: Only the \`body\` group supports size suffixes (e.g., \`body-small\`, \`body-medium\`).
427
+ - **Static Shorthands**: The following groups do NOT support size suffixes. Use the base shorthand only:
428
+ - **caption**: \`var(--text-caption-shorthand)\` (NEVER add -medium or -small)
429
+ - **display**: \`var(--text-display-shorthand)\`
430
+ - **codeBlock**: \`var(--text-codeBlock-shorthand)\`
431
+ - **codeInline**: \`var(--text-codeInline-shorthand)\`
432
+
433
+ ## 3. Logic Matrix: Color Pairings (CRITICAL)
434
+ | Background Token | Foreground Token | Requirement |
435
+ | :--- | :--- | :--- |
436
+ | --bgColor-*-emphasis | --fgColor-onEmphasis | MUST pair |
437
+ | --bgColor-*-muted | --fgColor-{semantic} | MUST match semantic |
438
+ | --bgColor-default | --fgColor-default | Standard pairing |
439
+ | --bgColor-muted | --fgColor-default | NEVER use fgColor-muted |
440
+
441
+ ## 4. Semantic Intent Key
442
+ Use these for \`find_tokens\` or to map natural language to groups:
443
+ - **Search Aliases**: Map "background" -> \`bgColor\`, "foreground" -> \`fgColor\`, "typography/font" -> \`text\`, "padding/margin" -> \`stack\`, "radius" -> \`borderRadius\`, "shadow/elevation" -> \`overlay\`.
444
+ - **danger**: Errors/Destructive | **success**: Positive/Done
445
+ - **attention**: Warning/Pending | **accent**: Interactive/Selected
446
+
447
+ ## 5. Available Token Groups
448
+ Use these names in \`find_tokens(group: "...")\`:
449
+ - **Semantic**: ${groups.semantic.map(g => g.name).join(', ')}
450
+ - **Components**: ${groups.component.map(g => g.name).join(', ')}
451
+
452
+ ## 6. Optimization Strategy (MANDATORY)
453
+ - **STOP**: Do not call \`find_tokens\` repeatedly for individual properties.
454
+ - **GO**: Use \`get_token_group_bundle\` to fetch relevant groups at once.
455
+ - *Example for Button*: \`get_token_group_bundle(groups: ["control", "button"])\`
456
+ - *Note*: \`control\` is for form inputs; \`button\` is for triggers.
457
+
458
+ ## 7. Token Bundle Recipes (Recommended)
459
+ - **Forms/Inputs**: \`["control", "focus", "outline", "text", "borderRadius"]\`
460
+ - **Modals/Dialogs**: \`["overlay", "shadow", "outline", "borderRadius", "bgColor"]\`
461
+ - **Data Tables**: \`["stack", "borderColor", "text", "bgColor"]\`
462
+ `.trim();
463
+ }
464
+
465
+ // Get token usage patterns text (static golden examples)
466
+ function getTokenUsagePatternsText() {
467
+ return `
468
+ # Design Token Reference Examples
469
+
470
+ > **CRITICAL FOR AI**: To implement the examples below, DO NOT search for tokens one-by-one.
471
+ > Use \`get_token_group_bundle(groups: ["control", "stack", "focus", "borderRadius"])\` to fetch the required token values in a single call.
472
+
473
+ ---
474
+
475
+ ## 1. Interaction Pattern: The Primary Button
476
+ *Demonstrates: 5 states, color pairing, typography shorthand, and motion.*
477
+
478
+ \`\`\`css
479
+ .btn-primary {
480
+ /* Logic: Use control tokens for interactive elements */
481
+ background-color: var(--control-bgColor-rest);
482
+ color: var(--fgColor-default);
483
+ font: var(--text-body-shorthand-medium); /* MUST use shorthand */
484
+
485
+ /* Scale: DEFAULT is medium/normal */
486
+ padding-block: var(--control-medium-paddingBlock);
487
+ padding-inline: var(--control-medium-paddingInline-normal);
488
+ border: none;
489
+ border-radius: var(--borderRadius-medium);
490
+ cursor: pointer;
491
+
492
+ /* Motion: MUST be <300ms */
493
+ transition: background-color 150ms ease, transform 100ms ease;
494
+ }
495
+
496
+ .btn-primary:hover {
497
+ background-color: var(--control-bgColor-hover);
498
+ }
499
+
500
+ .btn-primary:focus-visible {
501
+ outline: var(--focus-outline);
502
+ outline-offset: var(--outline-focus-offset);
503
+ }
504
+
505
+ .btn-primary:active {
506
+ background-color: var(--control-bgColor-active);
507
+ transform: scale(0.98);
508
+ }
509
+
510
+ .btn-primary:disabled {
511
+ /* Logic: MUST pair bgColor-disabled with fgColor-disabled */
512
+ background-color: var(--bgColor-disabled);
513
+ color: var(--fgColor-disabled);
514
+ cursor: not-allowed;
515
+ }
516
+ \`\`\`
517
+
518
+ ---
519
+
520
+ ## 2. Layout Pattern: Vertical Stack
521
+ *Demonstrates: Layout spacing rules and matching padding density.*
522
+
523
+ \`\`\`css
524
+ .card-stack {
525
+ display: flex;
526
+ flex-direction: column;
527
+
528
+ /* Logic: Use stack tokens for layout spacing */
529
+ gap: var(--stack-gap-normal);
530
+ padding: var(--stack-padding-normal);
531
+
532
+ background-color: var(--bgColor-default);
533
+ border: 1px solid var(--borderColor-default);
534
+ border-radius: var(--borderRadius-large);
535
+ }
536
+
537
+ /* Logic: Matching padding density to purpose */
538
+ .card-header {
539
+ padding-block-end: var(--stack-gap-condensed);
540
+ border-bottom: 1px solid var(--borderColor-muted);
541
+ }
542
+ \`\`\`
543
+
544
+ ---
545
+
546
+ ## Implementation Rules for AI:
547
+ 1. **Shorthand First**: Always use \`font: var(...)\` rather than splitting size/weight.
548
+ 2. **States**: Never implement a button without all 5 states.
549
+ 3. **Spacing**: Use \`control-\` tokens for the component itself and \`stack-\` tokens for the container/layout.
550
+ 4. **Motion**: Always include the \`prefers-reduced-motion\` media query to set transitions to \`none\`.
551
+ \`\`\`css
552
+ @media (prefers-reduced-motion: reduce) {
553
+ .btn-primary {
554
+ transition: none;
555
+ }
556
+ }
557
+ \`\`\`
558
+ `.trim();
559
+ }
560
+
561
+ // Search tokens with keyword matching and optional group filter
562
+ // Returns expanded tokens (patterns like [accent, danger] are expanded) filtered by query
563
+ function searchTokens(allTokens, query, group) {
564
+ // 1. Flatten and expand all patterns first (e.g., [accent, danger])
565
+ const expandedTokens = allTokens.flatMap(expandTokenPattern);
566
+
567
+ // 2. Prepare keywords and group filter
568
+ const keywords = query.toLowerCase().split(/\s+/).filter(k => k.length > 0);
569
+
570
+ // 3. Perform filtered search with keyword splitting (Logical AND)
571
+ return expandedTokens.filter(token => {
572
+ // Combine all relevant metadata into one searchable string
573
+ const searchableText = `${token.name} ${token.useCase} ${token.rules} ${token.group}`.toLowerCase();
574
+
575
+ // Ensure EVERY keyword in the query exists somewhere in this token's metadata
576
+ const matchesKeywords = keywords.every(word => searchableText.includes(word));
577
+ const matchesGroup = !group || tokenMatchesGroup(token, group);
578
+ return matchesKeywords && matchesGroup;
579
+ });
580
+ }
581
+
582
+ // Alias map: fuzzy/human-readable names → canonical token name prefix
583
+ const GROUP_ALIASES = {
584
+ // Identity mappings (canonical prefixes, lowercased key)
585
+ bgcolor: 'bgColor',
586
+ fgcolor: 'fgColor',
587
+ bordercolor: 'borderColor',
588
+ border: 'border',
589
+ shadow: 'shadow',
590
+ focus: 'focus',
591
+ color: 'color',
592
+ button: 'button',
593
+ control: 'control',
594
+ overlay: 'overlay',
595
+ borderradius: 'borderRadius',
596
+ boxshadow: 'boxShadow',
597
+ fontstack: 'fontStack',
598
+ spinner: 'spinner',
599
+ // Fuzzy aliases
600
+ background: 'bgColor',
601
+ backgroundcolor: 'bgColor',
602
+ bg: 'bgColor',
603
+ foreground: 'fgColor',
604
+ foregroundcolor: 'fgColor',
605
+ textcolor: 'fgColor',
606
+ fg: 'fgColor',
607
+ radius: 'borderRadius',
608
+ rounded: 'borderRadius',
609
+ elevation: 'overlay',
610
+ depth: 'overlay',
611
+ btn: 'button',
612
+ typography: 'text',
613
+ font: 'text',
614
+ text: 'text',
615
+ // Layout & Spacing
616
+ stack: 'stack',
617
+ controlstack: 'controlStack',
618
+ padding: 'stack',
619
+ margin: 'stack',
620
+ gap: 'stack',
621
+ spacing: 'stack',
622
+ layout: 'stack',
623
+ // State & Interaction
624
+ offset: 'focus',
625
+ outline: 'outline',
626
+ ring: 'focus',
627
+ // Decoration & Borders
628
+ borderwidth: 'borderWidth',
629
+ line: 'borderColor',
630
+ stroke: 'borderColor',
631
+ separator: 'borderColor'
632
+ };
633
+
634
+ // Match a token against a resolved group by checking both the token name prefix and the group label
635
+ function tokenMatchesGroup(token, resolvedGroup) {
636
+ const rg = resolvedGroup.toLowerCase();
637
+ const tokenPrefix = token.name.split('-')[0].toLowerCase();
638
+ const tokenGroup = token.group.toLowerCase();
639
+ return tokenPrefix === rg || tokenGroup === rg;
640
+ }
641
+
642
+ // Group tokens by their group property and format as Markdown
643
+ function formatBundle(bundleTokens) {
644
+ const grouped = bundleTokens.reduce((acc, token) => {
645
+ const group = GROUP_LABELS[token.group] || token.group || 'Ungrouped';
646
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
647
+ if (!acc[group]) acc[group] = [];
648
+ acc[group].push(token);
649
+ return acc;
650
+ }, {});
651
+ return Object.entries(grouped).map(([group, groupTokens]) => {
652
+ const tokenList = groupTokens.map(t => {
653
+ const nameLabel = t.value ? `\`${t.name}\` → \`${t.value}\`` : `\`${t.name}\``;
654
+ return `- ${nameLabel}\n - **U**: ${t.useCase || '(none)'}\n - **R**: ${t.rules || '(none)'}`;
655
+ }).join('\n');
656
+ return `## ${group}\n\n${tokenList}`;
657
+ }).join('\n\n---\n\n');
658
+ }
659
+
660
+ /**
661
+ * Generates a sorted, unique list of group names from the current token cache.
662
+ * Used for "Healing" error messages and the Design System Search Map.
663
+ */
664
+ function getValidGroupsList(validTokens) {
665
+ if (validTokens.length === 0) {
666
+ return 'No groups available.';
667
+ }
668
+
669
+ // 1. Extract unique group names
670
+ const uniqueGroups = Array.from(new Set(validTokens.map(t => t.group)));
671
+
672
+ // 2. Sort alphabetically for consistency
673
+ uniqueGroups.sort((a, b) => a.localeCompare(b));
674
+
675
+ // 3. Return as a formatted Markdown string with backticks
676
+ return uniqueGroups.map(g => `\`${g}\``).join(', ');
677
+ }
678
+
679
+ // Usage Guidance Hints
680
+ const groupHints = {
681
+ control: '`control` tokens are for form inputs/checkboxes. For buttons, use the `button` group.',
682
+ button: '`button` tokens are for standard triggers. For form-fields, see the `control` group.',
683
+ text: 'STRICT: The following typography groups do NOT support size suffixes (-small, -medium, -large): `caption`, `display`, `codeBlock`, and `codeInline`. Use the base shorthand name only (e.g., --text-codeBlock-shorthand).',
684
+ fgColor: 'Use `fgColor` for text. For borders, use `borderColor`.',
685
+ borderWidth: '`borderWidth` only has sizing values (thin, thick, thicker). For border *colors*, use the `borderColor` or `border` group.'
686
+ };
687
+
688
+ var version = "0.3.0";
689
+ var packageJson = {
690
+ version: version};
691
+
692
+ const server = new McpServer({
693
+ name: 'Primer',
694
+ version: packageJson.version
695
+ });
696
+ const turndownService = new TurndownService();
697
+
698
+ // Load all tokens with guidelines from primitives
699
+ const allTokensWithGuidelines = loadAllTokensWithGuidelines();
700
+
701
+ // -----------------------------------------------------------------------------
702
+ // Project setup
703
+ // -----------------------------------------------------------------------------
704
+ server.registerTool('init', {
705
+ description: 'Setup or create a project that includes Primer React'
706
+ }, async () => {
707
+ const url = new URL(`/product/getting-started/react`, 'https://primer.style');
708
+ const response = await fetch(url);
709
+ if (!response.ok) {
710
+ throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
711
+ }
712
+ const html = await response.text();
713
+ if (!html) {
714
+ return {
715
+ content: []
716
+ };
717
+ }
718
+ const $ = cheerio.load(html);
719
+ const source = $('main').html();
720
+ if (!source) {
721
+ return {
722
+ content: []
723
+ };
724
+ }
725
+ const text = turndownService.turndown(source);
726
+ return {
727
+ content: [{
728
+ type: 'text',
729
+ text: `The getting started documentation for Primer React is included below. It's important that the project:
730
+
731
+ - Is using a tool like Vite, Next.js, etc that supports TypeScript and React. If the project does not have support for that, generate an appropriate project scaffold
732
+ - Installs the latest version of \`@primer/react\` from \`npm\`
733
+ - Correctly adds the \`ThemeProvider\` and \`BaseStyles\` components to the root of the application
734
+ - Includes an import to a theme from \`@primer/primitives\`
735
+ - If the project wants to use icons, also install the \`@primer/octicons-react\` from \`npm\`
736
+ - Add appropriate agent instructions (like for copilot) to the project to prefer using components, tokens, icons, and more from Primer packages
737
+
738
+ ---
739
+
740
+ ${text}
741
+ `
742
+ }]
743
+ };
744
+ });
745
+
746
+ // -----------------------------------------------------------------------------
747
+ // Components
748
+ // -----------------------------------------------------------------------------
749
+ server.registerTool('list_components', {
750
+ description: 'List all of the components available from Primer React'
751
+ }, async () => {
752
+ const components = listComponents().map(component => {
753
+ return `- ${component.name}`;
754
+ });
755
+ return {
756
+ content: [{
757
+ type: 'text',
758
+ text: `The following components are available in the @primer/react in TypeScript projects:
759
+
760
+ ${components.join('\n')}
761
+
762
+ You can use the \`get_component\` tool to get more information about a specific component. You can use these components from the @primer/react package.`
763
+ }]
764
+ };
765
+ });
766
+ server.registerTool('get_component', {
767
+ description: 'Retrieve documentation and usage details for a specific React component from the @primer/react package by its name. This tool provides the official Primer documentation for any listed component, making it easy to inspect, reuse, or integrate components in your project.',
768
+ inputSchema: {
769
+ name: z.string().describe('The name of the component to retrieve')
770
+ }
771
+ }, async ({
772
+ name
773
+ }) => {
774
+ const components = listComponents();
775
+ const match = components.find(component => {
776
+ return component.name === name || component.name.toLowerCase() === name.toLowerCase();
777
+ });
778
+ if (!match) {
779
+ return {
780
+ isError: true,
781
+ errorMessage: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`list_components\` tool.`,
782
+ content: []
783
+ };
784
+ }
785
+ const llmsUrl = new URL(`/product/components/${match.slug}/llms.txt`, 'https://primer.style');
786
+ const llmsResponse = await fetch(llmsUrl);
787
+ if (llmsResponse.ok) {
788
+ try {
789
+ const llmsText = await llmsResponse.text();
790
+ return {
791
+ content: [{
792
+ type: 'text',
793
+ text: llmsText
794
+ }]
795
+ };
796
+ } catch (_) {
797
+ // If there's an error fetching or processing the llms.txt, we fall back to the regular documentation
798
+ }
799
+ }
800
+ const url = new URL(`/product/components/${match.slug}`, 'https://primer.style');
801
+ const response = await fetch(url);
802
+ if (!response.ok) {
803
+ throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
804
+ }
805
+ const html = await response.text();
806
+ if (!html) {
807
+ return {
808
+ content: []
809
+ };
810
+ }
811
+ const $ = cheerio.load(html);
812
+ const source = $('main').html();
813
+ if (!source) {
814
+ return {
815
+ content: []
816
+ };
817
+ }
818
+ const text = turndownService.turndown(source);
819
+ return {
820
+ content: [{
821
+ type: 'text',
822
+ text: `Here is the documentation for the \`${name}\` component from the @primer/react package:
823
+ ${text}`
824
+ }]
825
+ };
826
+ });
827
+ server.registerTool('get_component_examples', {
828
+ description: 'Get examples for how to use a component from Primer React',
829
+ inputSchema: {
830
+ name: z.string().describe('The name of the component to retrieve')
831
+ }
832
+ }, async ({
833
+ name
834
+ }) => {
835
+ const components = listComponents();
836
+ const match = components.find(component => {
837
+ return component.name === name;
838
+ });
839
+ if (!match) {
840
+ return {
841
+ content: [{
842
+ type: 'text',
843
+ text: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`get_components\` tool.`
844
+ }]
845
+ };
846
+ }
847
+ const url = new URL(`/product/components/${match.id}`, 'https://primer.style');
848
+ const response = await fetch(url);
849
+ if (!response.ok) {
850
+ throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
851
+ }
852
+ const html = await response.text();
853
+ if (!html) {
854
+ return {
855
+ content: []
856
+ };
857
+ }
858
+ const $ = cheerio.load(html);
859
+ const source = $('main').html();
860
+ if (!source) {
861
+ return {
862
+ content: []
863
+ };
864
+ }
865
+ const text = turndownService.turndown(source);
866
+ return {
867
+ content: [{
868
+ type: 'text',
869
+ text: `Here are some examples of how to use the \`${name}\` component from the @primer/react package:
870
+
871
+ ${text}`
872
+ }]
873
+ };
874
+ });
875
+ server.registerTool('get_component_usage_guidelines', {
876
+ description: 'Get usage information for how to use a component from Primer',
877
+ inputSchema: {
878
+ name: z.string().describe('The name of the component to retrieve')
879
+ }
880
+ }, async ({
881
+ name
882
+ }) => {
883
+ const components = listComponents();
884
+ const match = components.find(component => {
885
+ return component.name === name;
886
+ });
887
+ if (!match) {
888
+ return {
889
+ content: [{
890
+ type: 'text',
891
+ text: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`get_components\` tool.`
892
+ }]
893
+ };
894
+ }
895
+ const url = new URL(`/product/components/${match.id}/guidelines`, 'https://primer.style');
896
+ const response = await fetch(url);
897
+ if (!response.ok) {
898
+ if (response.status >= 400 && response.status < 500 || response.status >= 300 && response.status < 400) {
899
+ return {
900
+ content: [{
901
+ type: 'text',
902
+ text: `There are no accessibility guidelines for the \`${name}\` component in the @primer/react package.`
903
+ }]
904
+ };
905
+ }
906
+ throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
907
+ }
908
+ const html = await response.text();
909
+ if (!html) {
910
+ return {
911
+ content: []
912
+ };
913
+ }
914
+ const $ = cheerio.load(html);
915
+ const source = $('main').html();
916
+ if (!source) {
917
+ return {
918
+ content: []
919
+ };
920
+ }
921
+ const text = turndownService.turndown(source);
922
+ return {
923
+ content: [{
924
+ type: 'text',
925
+ text: `Here are the usage guidelines for the \`${name}\` component from the @primer/react package:
926
+
927
+ ${text}`
928
+ }]
929
+ };
930
+ });
931
+ server.registerTool('get_component_accessibility_guidelines', {
932
+ description: 'Retrieve accessibility guidelines and best practices for a specific component from the @primer/react package by its name. Use this tool to get official accessibility recommendations, usage tips, and requirements to ensure your UI components are inclusive and meet accessibility standards.',
933
+ inputSchema: {
934
+ name: z.string().describe('The name of the component to retrieve')
935
+ }
936
+ }, async ({
937
+ name
938
+ }) => {
939
+ const components = listComponents();
940
+ const match = components.find(component => {
941
+ return component.name === name;
942
+ });
943
+ if (!match) {
944
+ return {
945
+ content: [{
946
+ type: 'text',
947
+ text: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`list_components\` tool.`
948
+ }]
949
+ };
950
+ }
951
+ const url = new URL(`/product/components/${match.id}/accessibility`, 'https://primer.style');
952
+ const response = await fetch(url);
953
+ if (!response.ok) {
954
+ if (response.status >= 400 && response.status < 500 || response.status >= 300 && response.status < 400) {
955
+ return {
956
+ content: [{
957
+ type: 'text',
958
+ text: `There are no accessibility guidelines for the \`${name}\` component in the @primer/react package.`
959
+ }]
960
+ };
961
+ }
962
+ throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
963
+ }
964
+ const html = await response.text();
965
+ if (!html) {
966
+ return {
967
+ content: []
968
+ };
969
+ }
970
+ const $ = cheerio.load(html);
971
+ const source = $('main').html();
972
+ if (!source) {
973
+ return {
974
+ content: []
975
+ };
976
+ }
977
+ const text = turndownService.turndown(source);
978
+ return {
979
+ content: [{
980
+ type: 'text',
981
+ text: `Here are the accessibility guidelines for the \`${name}\` component from the @primer/react package:
982
+
983
+ ${text}`
984
+ }]
985
+ };
986
+ });
987
+
988
+ // -----------------------------------------------------------------------------
989
+ // Patterns
990
+ // -----------------------------------------------------------------------------
991
+ server.registerTool('list_patterns', {
992
+ description: 'List all of the patterns available from Primer React'
993
+ }, async () => {
994
+ const patterns = listPatterns().map(pattern => {
995
+ return `- ${pattern.name}`;
996
+ });
997
+ return {
998
+ content: [{
999
+ type: 'text',
1000
+ text: `The following patterns are available in the @primer/react in TypeScript projects:
1001
+
1002
+ ${patterns.join('\n')}`
1003
+ }]
1004
+ };
1005
+ });
1006
+ server.registerTool('get_pattern', {
1007
+ description: 'Get a specific pattern by name',
1008
+ inputSchema: {
1009
+ name: z.string().describe('The name of the pattern to retrieve')
1010
+ }
1011
+ }, async ({
1012
+ name
1013
+ }) => {
1014
+ const patterns = listPatterns();
1015
+ const match = patterns.find(pattern => {
1016
+ return pattern.name === name;
1017
+ });
1018
+ if (!match) {
1019
+ return {
1020
+ content: [{
1021
+ type: 'text',
1022
+ text: `There is no pattern named \`${name}\` in the @primer/react package. For a full list of patterns, use the \`list_patterns\` tool.`
1023
+ }]
1024
+ };
1025
+ }
1026
+ const url = new URL(`/product/ui-patterns/${match.id}`, 'https://primer.style');
1027
+ const response = await fetch(url);
1028
+ if (!response.ok) {
1029
+ throw new Error(`Failed to fetch ${url} - ${response.statusText}`);
1030
+ }
1031
+ const html = await response.text();
1032
+ if (!html) {
1033
+ return {
1034
+ content: []
1035
+ };
1036
+ }
1037
+ const $ = cheerio.load(html);
1038
+ const source = $('main').html();
1039
+ if (!source) {
1040
+ return {
1041
+ content: []
1042
+ };
1043
+ }
1044
+ const text = turndownService.turndown(source);
1045
+ return {
1046
+ content: [{
1047
+ type: 'text',
1048
+ text: `Here are the guidelines for the \`${name}\` pattern for Primer:
1049
+
1050
+ ${text}`
1051
+ }]
1052
+ };
1053
+ });
1054
+
1055
+ // -----------------------------------------------------------------------------
1056
+ // Design Tokens
1057
+ // -----------------------------------------------------------------------------
1058
+ server.registerTool('find_tokens', {
1059
+ description: "Search for specific tokens. Tip: If you only provide a 'group' and leave 'query' empty, it returns all tokens in that category. Avoid property-by-property searching.",
1060
+ inputSchema: {
1061
+ query: z.string().optional().default('').describe('Search keywords (e.g., "danger border", "success background")'),
1062
+ group: z.string().optional().describe('Filter by group (e.g., "fgColor", "border")'),
1063
+ limit: z.number().int().min(1).max(100).optional().default(15).describe('Maximum results to return to stay within context limits')
1064
+ }
1065
+ }, async ({
1066
+ query,
1067
+ group,
1068
+ limit
1069
+ }) => {
1070
+ // Resolve group via aliases
1071
+ const resolvedGroup = group ? GROUP_ALIASES[group.toLowerCase().replace(/\s+/g, '')] || group : undefined;
1072
+
1073
+ // Split query into keywords and extract any that match a known group
1074
+ const rawKeywords = query.toLowerCase().split(/\s+/).filter(k => k.length > 0);
1075
+ let effectiveGroup = resolvedGroup;
1076
+ const filteredKeywords = [];
1077
+ for (const kw of rawKeywords) {
1078
+ const normalized = kw.replace(/\s+/g, '');
1079
+ const aliasMatch = GROUP_ALIASES[normalized];
1080
+ if (aliasMatch && !effectiveGroup) {
1081
+ effectiveGroup = aliasMatch;
1082
+ } else {
1083
+ filteredKeywords.push(kw);
1084
+ }
1085
+ }
1086
+
1087
+ // Guard: no query and no group → ask user to provide at least one
1088
+ if (filteredKeywords.length === 0 && !effectiveGroup) {
1089
+ return {
1090
+ content: [{
1091
+ type: 'text',
1092
+ text: 'Please provide a query, a group, or both. Call `get_design_token_specs` to see available token groups.'
1093
+ }]
1094
+ };
1095
+ }
1096
+
1097
+ // Group-only search: return all tokens in the group
1098
+ const isGroupOnly = filteredKeywords.length === 0 && effectiveGroup;
1099
+ let results;
1100
+ if (isGroupOnly) {
1101
+ results = allTokensWithGuidelines.filter(token => tokenMatchesGroup(token, effectiveGroup));
1102
+ } else {
1103
+ results = searchTokens(allTokensWithGuidelines, filteredKeywords.join(' '), effectiveGroup);
1104
+ }
1105
+ if (results.length === 0) {
1106
+ const validGroups = getValidGroupsList(allTokensWithGuidelines);
1107
+ return {
1108
+ content: [{
1109
+ type: 'text',
1110
+ text: `No tokens found matching "${query}"${effectiveGroup ? ` in group "${effectiveGroup}"` : ''}.
1111
+
1112
+ ### 💡 Available Groups:
1113
+ ${validGroups}
1114
+
1115
+ ### Troubleshooting for AI:
1116
+ 1. **Multi-word Queries**: Search keywords use 'AND' logic. If searching "text shorthand typography" fails, try a single keyword like "shorthand" within the "text" group.
1117
+ 2. **Property Mismatch**: Do not search for CSS properties like "offset", "padding", or "font-size". Use semantic intent keywords: "danger", "muted", "emphasis".
1118
+ 3. **Typography**: Remember that \`caption\`, \`display\`, and \`code\` groups do NOT support size suffixes. Use the base shorthand only.
1119
+ 4. **Group Intent**: Use the \`group\` parameter instead of putting group names in the \`query\` string (e.g., use group: "stack" instead of query: "stack padding").`
1120
+ }]
1121
+ };
1122
+ }
1123
+ const limitedResults = results.slice(0, limit);
1124
+ let output;
1125
+ if (!query) {
1126
+ output = `Found ${results.length} token(s). Showing top ${limitedResults.length}:\n\n`;
1127
+ } else {
1128
+ output = `Found ${results.length} token(s) matching "${query}". Showing top ${limitedResults.length}:\n\n`;
1129
+ }
1130
+ output += formatBundle(limitedResults);
1131
+ if (results.length > limit) {
1132
+ output += `\n\n*...and ${results.length - limit} more matches. Use more specific keywords to narrow the search.*`;
1133
+ }
1134
+ return {
1135
+ content: [{
1136
+ type: 'text',
1137
+ text: output
1138
+ }]
1139
+ };
1140
+ });
1141
+ server.registerTool('get_token_group_bundle', {
1142
+ description: "PREFERRED FOR COMPONENTS. Fetch all tokens for complex UI (e.g., Dialogs, Cards) in one call by providing an array of groups like ['overlay', 'shadow']. Use this instead of multiple find_tokens calls to save context.",
1143
+ inputSchema: {
1144
+ groups: z.array(z.string()).describe('Array of group names (e.g., ["overlay", "shadow", "focus"])')
1145
+ }
1146
+ }, async ({
1147
+ groups
1148
+ }) => {
1149
+ // Normalize and resolve aliases
1150
+ const resolvedGroups = groups.map(g => {
1151
+ const normalized = g.toLowerCase().replace(/\s+/g, '');
1152
+ return GROUP_ALIASES[normalized] || g;
1153
+ });
1154
+
1155
+ // Filter tokens matching any of the resolved groups
1156
+ const matched = allTokensWithGuidelines.filter(token => resolvedGroups.some(rg => tokenMatchesGroup(token, rg)));
1157
+ if (matched.length === 0) {
1158
+ const validGroups = getValidGroupsList(allTokensWithGuidelines);
1159
+ return {
1160
+ content: [{
1161
+ type: 'text',
1162
+ text: `No tokens found for groups: ${groups.join(', ')}.\n\n### Valid Groups:\n${validGroups}`
1163
+ }]
1164
+ };
1165
+ }
1166
+ let text = `Found ${matched.length} token(s) across ${resolvedGroups.length} group(s):\n\n${formatBundle(matched)}`;
1167
+ const activeHints = resolvedGroups.map(g => groupHints[g]).filter(Boolean);
1168
+ if (activeHints.length > 0) {
1169
+ text += `\n\n### ⚠️ Usage Guidance:\n${activeHints.map(h => `- ${h}`).join('\n')}`;
1170
+ }
1171
+ return {
1172
+ content: [{
1173
+ type: 'text',
1174
+ text
1175
+ }]
1176
+ };
1177
+ });
1178
+ server.registerTool('get_design_token_specs', {
1179
+ description: 'CRITICAL: CALL THIS FIRST. Provides the logic matrix and the list of valid group names. You cannot search accurately without this map.'
1180
+ }, async () => {
1181
+ const groups = listTokenGroups();
1182
+ const customRules = getDesignTokenSpecsText(groups);
1183
+ let text;
1184
+ try {
1185
+ const upstreamGuide = loadDesignTokensGuide();
1186
+ text = `${customRules}\n\n---\n\n${upstreamGuide}`;
1187
+ } catch {
1188
+ text = customRules;
1189
+ }
1190
+ return {
1191
+ content: [{
1192
+ type: 'text',
1193
+ text
1194
+ }]
1195
+ };
1196
+ });
1197
+ server.registerTool('get_token_usage_patterns', {
1198
+ description: 'Provides "Golden Example" CSS for core patterns: Button (Interactions) and Stack (Layout). Use this to understand how to apply the Logic Matrix, Motion, and Spacing scales.'
1199
+ }, async () => {
1200
+ const customPatterns = getTokenUsagePatternsText();
1201
+ let text;
1202
+ try {
1203
+ const guide = loadDesignTokensGuide();
1204
+ const goldenExampleMatch = guide.match(/## Golden Example[\s\S]*?(?=\n## |$)/);
1205
+ if (goldenExampleMatch) {
1206
+ text = `${customPatterns}\n\n---\n\n${goldenExampleMatch[0].trim()}`;
1207
+ } else {
1208
+ text = customPatterns;
1209
+ }
1210
+ } catch {
1211
+ text = customPatterns;
1212
+ }
1213
+ return {
1214
+ content: [{
1215
+ type: 'text',
1216
+ text
1217
+ }]
1218
+ };
1219
+ });
1220
+
1221
+ // -----------------------------------------------------------------------------
1222
+ // Foundations
1223
+ // -----------------------------------------------------------------------------
1224
+ server.registerTool('get_color_usage', {
1225
+ description: 'Get the guidelines for how to apply color to a user interface'
1226
+ }, async () => {
1227
+ const url = new URL(`/product/getting-started/foundations/color-usage`, 'https://primer.style');
1228
+ const response = await fetch(url);
1229
+ if (!response.ok) {
1230
+ throw new Error(`Failed to fetch ${url} - ${response.statusText}`);
1231
+ }
1232
+ const html = await response.text();
1233
+ if (!html) {
1234
+ return {
1235
+ content: []
1236
+ };
1237
+ }
1238
+ const $ = cheerio.load(html);
1239
+ const source = $('main').html();
1240
+ if (!source) {
1241
+ return {
1242
+ content: []
1243
+ };
1244
+ }
1245
+ const text = turndownService.turndown(source);
1246
+ return {
1247
+ content: [{
1248
+ type: 'text',
1249
+ text: `Here is the documentation for color usage in Primer:\n\n${text}`
1250
+ }]
1251
+ };
1252
+ });
1253
+ server.registerTool('get_typography_usage', {
1254
+ description: 'Get the guidelines for how to apply typography to a user interface'
1255
+ }, async () => {
1256
+ const url = new URL(`/product/getting-started/foundations/typography`, 'https://primer.style');
1257
+ const response = await fetch(url);
1258
+ if (!response.ok) {
1259
+ throw new Error(`Failed to fetch ${url} - ${response.statusText}`);
1260
+ }
1261
+ const html = await response.text();
1262
+ if (!html) {
1263
+ return {
1264
+ content: []
1265
+ };
1266
+ }
1267
+ const $ = cheerio.load(html);
1268
+ const source = $('main').html();
1269
+ if (!source) {
1270
+ return {
1271
+ content: []
1272
+ };
1273
+ }
1274
+ const text = turndownService.turndown(source);
1275
+ return {
1276
+ content: [{
1277
+ type: 'text',
1278
+ text: `Here is the documentation for typography usage in Primer:\n\n${text}`
1279
+ }]
1280
+ };
1281
+ });
1282
+
1283
+ // -----------------------------------------------------------------------------
1284
+ // Icons
1285
+ // -----------------------------------------------------------------------------
1286
+ server.registerTool('list_icons', {
1287
+ description: 'List all of the icons (octicons) available from Primer Octicons React'
1288
+ }, async () => {
1289
+ const icons = listIcons().map(icon => {
1290
+ const keywords = icon.keywords.map(keyword => {
1291
+ return `<keyword>${keyword}</keyword>`;
1292
+ });
1293
+ const sizes = icon.heights.map(height => {
1294
+ return `<size value="${height}"></size>`;
1295
+ });
1296
+ return [`<icon name="${icon.name}">`, ...keywords, ...sizes, `</icon>`].join('\n');
1297
+ });
1298
+ return {
1299
+ content: [{
1300
+ type: 'text',
1301
+ text: `The following icons are available in the @primer/octicons-react package in TypeScript projects:
1302
+
1303
+ ${icons.join('\n')}
1304
+
1305
+ You can use the \`get_icon\` tool to get more information about a specific icon. You can use these components from the @primer/octicons-react package.`
1306
+ }]
1307
+ };
1308
+ });
1309
+ server.registerTool('get_icon', {
1310
+ description: 'Get a specific icon (octicon) by name from Primer',
1311
+ inputSchema: {
1312
+ name: z.string().describe('The name of the icon to retrieve'),
1313
+ size: z.string().optional().describe('The size of the icon to retrieve, e.g. "16"').default('16')
1314
+ }
1315
+ }, async ({
1316
+ name,
1317
+ size
1318
+ }) => {
1319
+ const icons = listIcons();
1320
+ const match = icons.find(icon => {
1321
+ return icon.name === name || icon.name.toLowerCase() === name.toLowerCase();
1322
+ });
1323
+ if (!match) {
1324
+ return {
1325
+ content: [{
1326
+ type: 'text',
1327
+ text: `There is no icon named \`${name}\` in the @primer/octicons-react package. For a full list of icons, use the \`get_icon\` tool.`
1328
+ }]
1329
+ };
1330
+ }
1331
+ const url = new URL(`/octicons/icon/${match.name}-${size}`, 'https://primer.style');
1332
+ const response = await fetch(url);
1333
+ if (!response.ok) {
1334
+ throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
1335
+ }
1336
+ const html = await response.text();
1337
+ if (!html) {
1338
+ return {
1339
+ content: []
1340
+ };
1341
+ }
1342
+ const $ = cheerio.load(html);
1343
+ const source = $('main').html();
1344
+ if (!source) {
1345
+ return {
1346
+ content: []
1347
+ };
1348
+ }
1349
+ const text = turndownService.turndown(source);
1350
+ return {
1351
+ content: [{
1352
+ type: 'text',
1353
+ text: `Here is the documentation for the \`${name}\` icon at size: \`${size}\`:
1354
+ ${text}`
1355
+ }]
1356
+ };
1357
+ });
1358
+
1359
+ // -----------------------------------------------------------------------------
1360
+ // Coding guidelines
1361
+ // -----------------------------------------------------------------------------
1362
+ server.registerTool('primer_coding_guidelines', {
1363
+ description: 'Get the guidelines when writing code that uses Primer or for UI code that you are creating'
1364
+ }, async () => {
1365
+ return {
1366
+ content: [{
1367
+ type: 'text',
1368
+ text: `When writing code that uses Primer, follow these guidelines:
1369
+
1370
+ ## Design Tokens
1371
+
1372
+ - Prefer design tokens over hard-coded values. For example, use \`var(--fgColor-default)\` instead of \`#24292f\`. Use the \`find_tokens\` tool to search for a design token by keyword or group. Use \`get_design_token_specs\` to browse available token groups, and \`get_token_group_bundle\` to retrieve all tokens within a specific group.
1373
+ - Prefer recommending design tokens in the same group for related CSS properties. For example, when styling background and border color, use tokens from the same group/category
1374
+
1375
+ ## Authoring & Using Components
1376
+
1377
+ - Prefer re-using a component from Primer when possible over writing a new component.
1378
+ - Prefer using existing props for a component for styling instead of adding styling to a component
1379
+ - Prefer using icons from Primer instead of creating new icons. Use the \`list_icons\` tool to find the icon you need.
1380
+ - Follow patterns from Primer when creating new components. Use the \`list_patterns\` tool to find the pattern you need, if one exists
1381
+ - When using a component from Primer, make sure to follow the component's usage and accessibility guidelines
1382
+
1383
+ ## Coding guidelines
1384
+
1385
+ The following list of coding guidelines must be followed:
1386
+
1387
+ - Do not use the sx prop for styling components. Instead, use CSS Modules.
1388
+ - Do not use the Box component for styling components. Instead, use CSS Modules.
1389
+ `
1390
+ }]
1391
+ };
1392
+ });
1393
+
1394
+ // -----------------------------------------------------------------------------
1395
+ // Accessibility
1396
+ // -----------------------------------------------------------------------------
1397
+
1398
+ /**
1399
+ * The `review_alt_text` tool is experimental and may be removed in future versions.
1400
+ *
1401
+ * The intent of this tool is to assist products like Copilot Code Review and Copilot Coding Agent
1402
+ * in reviewing both user- and AI-generated alt text for images, ensuring compliance with accessibility guidelines.
1403
+ * This tool is not intended to replace human-generated alt text; rather, it supports the review process
1404
+ * by providing suggestions for improvement. It should be used alongside human review, not as a substitute.
1405
+ *
1406
+ *
1407
+ **/
1408
+ server.registerTool('review_alt_text', {
1409
+ description: 'Evaluates image alt text against accessibility best practices and context relevance.',
1410
+ inputSchema: {
1411
+ surroundingText: z.string().describe('Text surrounding the image, relevant to the image.'),
1412
+ alt: z.string().describe('The alt text of the image being evaluated'),
1413
+ image: z.string().describe('The image URL or file path being evaluated')
1414
+ }
1415
+ }, async ({
1416
+ surroundingText,
1417
+ alt,
1418
+ image
1419
+ }) => {
1420
+ // Call the LLM through MCP sampling
1421
+ const response = await server.server.createMessage({
1422
+ messages: [{
1423
+ role: 'user',
1424
+ content: {
1425
+ type: 'text',
1426
+ text: `Does this alt text: '${alt}' meet accessibility guidelines and describe the image: ${image} accurately in context of this surrounding text: '${surroundingText}'?\n\n`
1427
+ }
1428
+ }],
1429
+ sampling: {
1430
+ temperature: 0.4
1431
+ },
1432
+ maxTokens: 500
1433
+ });
1434
+ return {
1435
+ content: [{
1436
+ type: 'text',
1437
+ text: response.content.type === 'text' ? response.content.text : 'Unable to generate summary'
1438
+ }],
1439
+ altTextEvaluation: response.content.type === 'text' ? response.content.text : 'Unable to generate summary',
1440
+ nextSteps: `If the evaluation indicates issues with the alt text, provide more meaningful alt text based on the feedback. DO NOT run this tool repeatedly on the same image - evaluations may vary slightly with each run.`
1441
+ };
1442
+ });
1443
+
1444
+ export { server as s };