@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/dist/index.js +3 -1
- package/dist/primitives.d.ts +44 -1
- package/dist/primitives.d.ts.map +1 -1
- package/dist/server-Cwz0naYT.js +1444 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/stdio.js +3 -1
- package/package.json +2 -2
- package/src/primitives.ts +626 -1
- package/src/server.ts +222 -17
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 {
|
|
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(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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 \`
|
|
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
|