@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.
package/src/server.ts CHANGED
@@ -5,7 +5,20 @@ import * as cheerio from 'cheerio'
5
5
  import * as z from 'zod'
6
6
  import TurndownService from 'turndown'
7
7
  import {listComponents, listPatterns, listIcons} from './primer'
8
- import {tokens, serialize} from './primitives'
8
+ import {
9
+ listTokenGroups,
10
+ loadAllTokensWithGuidelines,
11
+ loadDesignTokensGuide,
12
+ getDesignTokenSpecsText,
13
+ getTokenUsagePatternsText,
14
+ searchTokens,
15
+ formatBundle,
16
+ GROUP_ALIASES,
17
+ tokenMatchesGroup,
18
+ type TokenWithGuidelines,
19
+ getValidGroupsList,
20
+ groupHints,
21
+ } from './primitives'
9
22
  import packageJson from '../package.json' with {type: 'json'}
10
23
 
11
24
  const server = new McpServer({
@@ -15,6 +28,9 @@ const server = new McpServer({
15
28
 
16
29
  const turndownService = new TurndownService()
17
30
 
31
+ // Load all tokens with guidelines from primitives
32
+ const allTokensWithGuidelines: TokenWithGuidelines[] = loadAllTokensWithGuidelines()
33
+
18
34
  // -----------------------------------------------------------------------------
19
35
  // Project setup
20
36
  // -----------------------------------------------------------------------------
@@ -459,21 +475,210 @@ ${text}`,
459
475
  // -----------------------------------------------------------------------------
460
476
  // Design Tokens
461
477
  // -----------------------------------------------------------------------------
462
- server.registerTool('list_tokens', {description: 'List all of the design tokens available from Primer'}, async () => {
463
- let text =
464
- 'Below is a list of all design tokens available from Primer. Tokens are used in CSS and CSS Modules. To refer to the CSS Custom Property for a design token, wrap it in var(--{name-of-token}). To learn how to use a specific token, use a corresponding usage tool for the category of the token. For example, if a token is a color token look for the get_color_usage tool. \n\n'
465
-
466
- text += serialize(tokens)
467
-
468
- return {
469
- content: [
470
- {
471
- type: 'text',
472
- text,
473
- },
474
- ],
475
- }
476
- })
478
+ server.registerTool(
479
+ 'find_tokens',
480
+ {
481
+ description:
482
+ "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.",
483
+ inputSchema: {
484
+ query: z
485
+ .string()
486
+ .optional()
487
+ .default('')
488
+ .describe('Search keywords (e.g., "danger border", "success background")'),
489
+ group: z.string().optional().describe('Filter by group (e.g., "fgColor", "border")'),
490
+ limit: z
491
+ .number()
492
+ .int()
493
+ .min(1)
494
+ .max(100)
495
+ .optional()
496
+ .default(15)
497
+ .describe('Maximum results to return to stay within context limits'),
498
+ },
499
+ },
500
+ async ({query, group, limit}) => {
501
+ // Resolve group via aliases
502
+ const resolvedGroup = group ? GROUP_ALIASES[group.toLowerCase().replace(/\s+/g, '')] || group : undefined
503
+
504
+ // Split query into keywords and extract any that match a known group
505
+ const rawKeywords = query
506
+ .toLowerCase()
507
+ .split(/\s+/)
508
+ .filter(k => k.length > 0)
509
+
510
+ let effectiveGroup = resolvedGroup
511
+ const filteredKeywords: string[] = []
512
+
513
+ for (const kw of rawKeywords) {
514
+ const normalized = kw.replace(/\s+/g, '')
515
+ const aliasMatch = GROUP_ALIASES[normalized]
516
+ if (aliasMatch && !effectiveGroup) {
517
+ effectiveGroup = aliasMatch
518
+ } else {
519
+ filteredKeywords.push(kw)
520
+ }
521
+ }
522
+
523
+ // Guard: no query and no group → ask user to provide at least one
524
+ if (filteredKeywords.length === 0 && !effectiveGroup) {
525
+ return {
526
+ content: [
527
+ {
528
+ type: 'text',
529
+ text: 'Please provide a query, a group, or both. Call `get_design_token_specs` to see available token groups.',
530
+ },
531
+ ],
532
+ }
533
+ }
534
+
535
+ // Group-only search: return all tokens in the group
536
+ const isGroupOnly = filteredKeywords.length === 0 && effectiveGroup
537
+ let results: TokenWithGuidelines[]
538
+
539
+ if (isGroupOnly) {
540
+ results = allTokensWithGuidelines.filter(token => tokenMatchesGroup(token, effectiveGroup!))
541
+ } else {
542
+ results = searchTokens(allTokensWithGuidelines, filteredKeywords.join(' '), effectiveGroup)
543
+ }
544
+
545
+ if (results.length === 0) {
546
+ const validGroups = getValidGroupsList(allTokensWithGuidelines)
547
+
548
+ return {
549
+ content: [
550
+ {
551
+ type: 'text',
552
+ text: `No tokens found matching "${query}"${effectiveGroup ? ` in group "${effectiveGroup}"` : ''}.
553
+
554
+ ### 💡 Available Groups:
555
+ ${validGroups}
556
+
557
+ ### Troubleshooting for AI:
558
+ 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.
559
+ 2. **Property Mismatch**: Do not search for CSS properties like "offset", "padding", or "font-size". Use semantic intent keywords: "danger", "muted", "emphasis".
560
+ 3. **Typography**: Remember that \`caption\`, \`display\`, and \`code\` groups do NOT support size suffixes. Use the base shorthand only.
561
+ 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").`,
562
+ },
563
+ ],
564
+ }
565
+ }
566
+
567
+ const limitedResults = results.slice(0, limit)
568
+
569
+ let output: string
570
+
571
+ if (!query) {
572
+ output = `Found ${results.length} token(s). Showing top ${limitedResults.length}:\n\n`
573
+ } else {
574
+ output = `Found ${results.length} token(s) matching "${query}". Showing top ${limitedResults.length}:\n\n`
575
+ }
576
+ output += formatBundle(limitedResults)
577
+
578
+ if (results.length > limit) {
579
+ output += `\n\n*...and ${results.length - limit} more matches. Use more specific keywords to narrow the search.*`
580
+ }
581
+
582
+ return {
583
+ content: [{type: 'text', text: output}],
584
+ }
585
+ },
586
+ )
587
+
588
+ server.registerTool(
589
+ 'get_token_group_bundle',
590
+ {
591
+ description:
592
+ "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.",
593
+ inputSchema: {
594
+ groups: z.array(z.string()).describe('Array of group names (e.g., ["overlay", "shadow", "focus"])'),
595
+ },
596
+ },
597
+ async ({groups}) => {
598
+ // Normalize and resolve aliases
599
+ const resolvedGroups = groups.map(g => {
600
+ const normalized = g.toLowerCase().replace(/\s+/g, '')
601
+ return GROUP_ALIASES[normalized] || g
602
+ })
603
+
604
+ // Filter tokens matching any of the resolved groups
605
+ const matched = allTokensWithGuidelines.filter(token => resolvedGroups.some(rg => tokenMatchesGroup(token, rg)))
606
+
607
+ if (matched.length === 0) {
608
+ const validGroups = getValidGroupsList(allTokensWithGuidelines)
609
+ return {
610
+ content: [
611
+ {
612
+ type: 'text',
613
+ text: `No tokens found for groups: ${groups.join(', ')}.\n\n### Valid Groups:\n${validGroups}`,
614
+ },
615
+ ],
616
+ }
617
+ }
618
+
619
+ let text = `Found ${matched.length} token(s) across ${resolvedGroups.length} group(s):\n\n${formatBundle(matched)}`
620
+
621
+ const activeHints = resolvedGroups.map(g => groupHints[g]).filter(Boolean)
622
+
623
+ if (activeHints.length > 0) {
624
+ text += `\n\n### ⚠️ Usage Guidance:\n${activeHints.map(h => `- ${h}`).join('\n')}`
625
+ }
626
+
627
+ return {
628
+ content: [{type: 'text', text}],
629
+ }
630
+ },
631
+ )
632
+
633
+ server.registerTool(
634
+ 'get_design_token_specs',
635
+ {
636
+ description:
637
+ 'CRITICAL: CALL THIS FIRST. Provides the logic matrix and the list of valid group names. You cannot search accurately without this map.',
638
+ },
639
+ async () => {
640
+ const groups = listTokenGroups()
641
+ const customRules = getDesignTokenSpecsText(groups)
642
+ let text: string
643
+ try {
644
+ const upstreamGuide = loadDesignTokensGuide()
645
+ text = `${customRules}\n\n---\n\n${upstreamGuide}`
646
+ } catch {
647
+ text = customRules
648
+ }
649
+
650
+ return {
651
+ content: [{type: 'text', text}],
652
+ }
653
+ },
654
+ )
655
+
656
+ server.registerTool(
657
+ 'get_token_usage_patterns',
658
+ {
659
+ description:
660
+ '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.',
661
+ },
662
+ async () => {
663
+ const customPatterns = getTokenUsagePatternsText()
664
+ let text: string
665
+ try {
666
+ const guide = loadDesignTokensGuide()
667
+ const goldenExampleMatch = guide.match(/## Golden Example[\s\S]*?(?=\n## |$)/)
668
+ if (goldenExampleMatch) {
669
+ text = `${customPatterns}\n\n---\n\n${goldenExampleMatch[0].trim()}`
670
+ } else {
671
+ text = customPatterns
672
+ }
673
+ } catch {
674
+ text = customPatterns
675
+ }
676
+
677
+ return {
678
+ content: [{type: 'text', text}],
679
+ }
680
+ },
681
+ )
477
682
 
478
683
  // -----------------------------------------------------------------------------
479
684
  // Foundations
@@ -661,7 +866,7 @@ server.registerTool(
661
866
 
662
867
  ## Design Tokens
663
868
 
664
- - Prefer design tokens over hard-coded values. For example, use \`var(--fgColor-default)\` instead of \`#24292f\`. Use the \`list_tokens\` tool to find the design token you need.
869
+ - 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.
665
870
  - 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
666
871
 
667
872
  ## Authoring & Using Components