@primer/mcp 0.0.5 → 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/primer.d.ts.map +1 -1
- package/dist/primitives.d.ts +44 -1
- package/dist/primitives.d.ts.map +1 -1
- package/dist/{server-BzATDQ5A.js → server-CjO5UCV7.js} +82 -34
- package/dist/server-Cwz0naYT.js +1444 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/stdio.js +3 -1
- package/package.json +17 -13
- package/src/primer.ts +0 -4
- package/src/primitives.ts +626 -1
- package/src/server.ts +439 -182
- package/dist/server-Ccnupv1s.js +0 -718
package/src/server.ts
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
2
|
// eslint-disable-next-line import/no-namespace
|
|
3
3
|
import * as cheerio from 'cheerio'
|
|
4
|
-
|
|
4
|
+
// eslint-disable-next-line import/no-namespace
|
|
5
|
+
import * as z from 'zod'
|
|
5
6
|
import TurndownService from 'turndown'
|
|
6
7
|
import {listComponents, listPatterns, listIcons} from './primer'
|
|
7
|
-
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'
|
|
8
22
|
import packageJson from '../package.json' with {type: 'json'}
|
|
9
23
|
|
|
10
24
|
const server = new McpServer({
|
|
@@ -14,38 +28,46 @@ const server = new McpServer({
|
|
|
14
28
|
|
|
15
29
|
const turndownService = new TurndownService()
|
|
16
30
|
|
|
31
|
+
// Load all tokens with guidelines from primitives
|
|
32
|
+
const allTokensWithGuidelines: TokenWithGuidelines[] = loadAllTokensWithGuidelines()
|
|
33
|
+
|
|
17
34
|
// -----------------------------------------------------------------------------
|
|
18
35
|
// Project setup
|
|
19
36
|
// -----------------------------------------------------------------------------
|
|
20
|
-
server.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
content: [],
|
|
37
|
+
server.registerTool(
|
|
38
|
+
'init',
|
|
39
|
+
{
|
|
40
|
+
description: 'Setup or create a project that includes Primer React',
|
|
41
|
+
},
|
|
42
|
+
async () => {
|
|
43
|
+
const url = new URL(`/product/getting-started/react`, 'https://primer.style')
|
|
44
|
+
const response = await fetch(url)
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(`Failed to fetch ${url}: ${response.statusText}`)
|
|
31
47
|
}
|
|
32
|
-
}
|
|
33
48
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
const html = await response.text()
|
|
50
|
+
if (!html) {
|
|
51
|
+
return {
|
|
52
|
+
content: [],
|
|
53
|
+
}
|
|
39
54
|
}
|
|
40
|
-
}
|
|
41
55
|
|
|
42
|
-
|
|
56
|
+
const $ = cheerio.load(html)
|
|
57
|
+
const source = $('main').html()
|
|
58
|
+
if (!source) {
|
|
59
|
+
return {
|
|
60
|
+
content: [],
|
|
61
|
+
}
|
|
62
|
+
}
|
|
43
63
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
const text = turndownService.turndown(source)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: 'text',
|
|
70
|
+
text: `The getting started documentation for Primer React is included below. It's important that the project:
|
|
49
71
|
|
|
50
72
|
- 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
|
|
51
73
|
- Installs the latest version of \`@primer/react\` from \`npm\`
|
|
@@ -58,37 +80,45 @@ server.tool('init', 'Setup or create a project that includes Primer React', asyn
|
|
|
58
80
|
|
|
59
81
|
${text}
|
|
60
82
|
`,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
)
|
|
65
88
|
|
|
66
89
|
// -----------------------------------------------------------------------------
|
|
67
90
|
// Components
|
|
68
91
|
// -----------------------------------------------------------------------------
|
|
69
|
-
server.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
92
|
+
server.registerTool(
|
|
93
|
+
'list_components',
|
|
94
|
+
{description: 'List all of the components available from Primer React'},
|
|
95
|
+
async () => {
|
|
96
|
+
const components = listComponents().map(component => {
|
|
97
|
+
return `- ${component.name}`
|
|
98
|
+
})
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: 'text',
|
|
103
|
+
text: `The following components are available in the @primer/react in TypeScript projects:
|
|
78
104
|
|
|
79
105
|
${components.join('\n')}
|
|
80
106
|
|
|
81
107
|
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.`,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
)
|
|
86
113
|
|
|
87
|
-
server.
|
|
114
|
+
server.registerTool(
|
|
88
115
|
'get_component',
|
|
89
|
-
'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.',
|
|
90
116
|
{
|
|
91
|
-
|
|
117
|
+
description:
|
|
118
|
+
'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.',
|
|
119
|
+
inputSchema: {
|
|
120
|
+
name: z.string().describe('The name of the component to retrieve'),
|
|
121
|
+
},
|
|
92
122
|
},
|
|
93
123
|
async ({name}) => {
|
|
94
124
|
const components = listComponents()
|
|
@@ -97,12 +127,27 @@ server.tool(
|
|
|
97
127
|
})
|
|
98
128
|
if (!match) {
|
|
99
129
|
return {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
130
|
+
isError: true,
|
|
131
|
+
errorMessage: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`list_components\` tool.`,
|
|
132
|
+
content: [],
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const llmsUrl = new URL(`/product/components/${match.slug}/llms.txt`, 'https://primer.style')
|
|
137
|
+
const llmsResponse = await fetch(llmsUrl)
|
|
138
|
+
if (llmsResponse.ok) {
|
|
139
|
+
try {
|
|
140
|
+
const llmsText = await llmsResponse.text()
|
|
141
|
+
return {
|
|
142
|
+
content: [
|
|
143
|
+
{
|
|
144
|
+
type: 'text',
|
|
145
|
+
text: llmsText,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
}
|
|
149
|
+
} catch (_: unknown) {
|
|
150
|
+
// If there's an error fetching or processing the llms.txt, we fall back to the regular documentation
|
|
106
151
|
}
|
|
107
152
|
}
|
|
108
153
|
|
|
@@ -141,11 +186,13 @@ ${text}`,
|
|
|
141
186
|
},
|
|
142
187
|
)
|
|
143
188
|
|
|
144
|
-
server.
|
|
189
|
+
server.registerTool(
|
|
145
190
|
'get_component_examples',
|
|
146
|
-
'Get examples for how to use a component from Primer React',
|
|
147
191
|
{
|
|
148
|
-
|
|
192
|
+
description: 'Get examples for how to use a component from Primer React',
|
|
193
|
+
inputSchema: {
|
|
194
|
+
name: z.string().describe('The name of the component to retrieve'),
|
|
195
|
+
},
|
|
149
196
|
},
|
|
150
197
|
async ({name}) => {
|
|
151
198
|
const components = listComponents()
|
|
@@ -199,11 +246,13 @@ ${text}`,
|
|
|
199
246
|
},
|
|
200
247
|
)
|
|
201
248
|
|
|
202
|
-
server.
|
|
249
|
+
server.registerTool(
|
|
203
250
|
'get_component_usage_guidelines',
|
|
204
|
-
'Get usage information for how to use a component from Primer',
|
|
205
251
|
{
|
|
206
|
-
|
|
252
|
+
description: 'Get usage information for how to use a component from Primer',
|
|
253
|
+
inputSchema: {
|
|
254
|
+
name: z.string().describe('The name of the component to retrieve'),
|
|
255
|
+
},
|
|
207
256
|
},
|
|
208
257
|
async ({name}) => {
|
|
209
258
|
const components = listComponents()
|
|
@@ -268,11 +317,14 @@ ${text}`,
|
|
|
268
317
|
},
|
|
269
318
|
)
|
|
270
319
|
|
|
271
|
-
server.
|
|
320
|
+
server.registerTool(
|
|
272
321
|
'get_component_accessibility_guidelines',
|
|
273
|
-
'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.',
|
|
274
322
|
{
|
|
275
|
-
|
|
323
|
+
description:
|
|
324
|
+
'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.',
|
|
325
|
+
inputSchema: {
|
|
326
|
+
name: z.string().describe('The name of the component to retrieve'),
|
|
327
|
+
},
|
|
276
328
|
},
|
|
277
329
|
async ({name}) => {
|
|
278
330
|
const components = listComponents()
|
|
@@ -340,27 +392,33 @@ ${text}`,
|
|
|
340
392
|
// -----------------------------------------------------------------------------
|
|
341
393
|
// Patterns
|
|
342
394
|
// -----------------------------------------------------------------------------
|
|
343
|
-
server.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
395
|
+
server.registerTool(
|
|
396
|
+
'list_patterns',
|
|
397
|
+
{description: 'List all of the patterns available from Primer React'},
|
|
398
|
+
async () => {
|
|
399
|
+
const patterns = listPatterns().map(pattern => {
|
|
400
|
+
return `- ${pattern.name}`
|
|
401
|
+
})
|
|
402
|
+
return {
|
|
403
|
+
content: [
|
|
404
|
+
{
|
|
405
|
+
type: 'text',
|
|
406
|
+
text: `The following patterns are available in the @primer/react in TypeScript projects:
|
|
352
407
|
|
|
353
408
|
${patterns.join('\n')}`,
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
)
|
|
358
414
|
|
|
359
|
-
server.
|
|
415
|
+
server.registerTool(
|
|
360
416
|
'get_pattern',
|
|
361
|
-
'Get a specific pattern by name',
|
|
362
417
|
{
|
|
363
|
-
|
|
418
|
+
description: 'Get a specific pattern by name',
|
|
419
|
+
inputSchema: {
|
|
420
|
+
name: z.string().describe('The name of the pattern to retrieve'),
|
|
421
|
+
},
|
|
364
422
|
},
|
|
365
423
|
async ({name}) => {
|
|
366
424
|
const patterns = listPatterns()
|
|
@@ -417,127 +475,330 @@ ${text}`,
|
|
|
417
475
|
// -----------------------------------------------------------------------------
|
|
418
476
|
// Design Tokens
|
|
419
477
|
// -----------------------------------------------------------------------------
|
|
420
|
-
server.
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
+
}
|
|
435
581
|
|
|
436
|
-
// -----------------------------------------------------------------------------
|
|
437
|
-
// Foundations
|
|
438
|
-
// -----------------------------------------------------------------------------
|
|
439
|
-
server.tool('get_color_usage', 'Get the guidelines for how to apply color to a user interface', async () => {
|
|
440
|
-
const url = new URL(`/product/getting-started/foundations/color-usage`, 'https://primer.style')
|
|
441
|
-
const response = await fetch(url)
|
|
442
|
-
if (!response.ok) {
|
|
443
|
-
throw new Error(`Failed to fetch ${url} - ${response.statusText}`)
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const html = await response.text()
|
|
447
|
-
if (!html) {
|
|
448
582
|
return {
|
|
449
|
-
content: [],
|
|
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')}`
|
|
450
625
|
}
|
|
451
|
-
}
|
|
452
626
|
|
|
453
|
-
const $ = cheerio.load(html)
|
|
454
|
-
const source = $('main').html()
|
|
455
|
-
if (!source) {
|
|
456
627
|
return {
|
|
457
|
-
content: [],
|
|
628
|
+
content: [{type: 'text', text}],
|
|
458
629
|
}
|
|
459
|
-
}
|
|
630
|
+
},
|
|
631
|
+
)
|
|
460
632
|
|
|
461
|
-
|
|
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
|
+
}
|
|
462
649
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
},
|
|
469
|
-
],
|
|
470
|
-
}
|
|
471
|
-
})
|
|
650
|
+
return {
|
|
651
|
+
content: [{type: 'text', text}],
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
)
|
|
472
655
|
|
|
473
|
-
server.
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
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
|
+
}
|
|
479
676
|
|
|
480
|
-
const html = await response.text()
|
|
481
|
-
if (!html) {
|
|
482
677
|
return {
|
|
483
|
-
content: [],
|
|
678
|
+
content: [{type: 'text', text}],
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
// -----------------------------------------------------------------------------
|
|
684
|
+
// Foundations
|
|
685
|
+
// -----------------------------------------------------------------------------
|
|
686
|
+
server.registerTool(
|
|
687
|
+
'get_color_usage',
|
|
688
|
+
{description: 'Get the guidelines for how to apply color to a user interface'},
|
|
689
|
+
async () => {
|
|
690
|
+
const url = new URL(`/product/getting-started/foundations/color-usage`, 'https://primer.style')
|
|
691
|
+
const response = await fetch(url)
|
|
692
|
+
if (!response.ok) {
|
|
693
|
+
throw new Error(`Failed to fetch ${url} - ${response.statusText}`)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const html = await response.text()
|
|
697
|
+
if (!html) {
|
|
698
|
+
return {
|
|
699
|
+
content: [],
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const $ = cheerio.load(html)
|
|
704
|
+
const source = $('main').html()
|
|
705
|
+
if (!source) {
|
|
706
|
+
return {
|
|
707
|
+
content: [],
|
|
708
|
+
}
|
|
484
709
|
}
|
|
485
|
-
}
|
|
486
710
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (!source) {
|
|
711
|
+
const text = turndownService.turndown(source)
|
|
712
|
+
|
|
490
713
|
return {
|
|
491
|
-
content: [
|
|
714
|
+
content: [
|
|
715
|
+
{
|
|
716
|
+
type: 'text',
|
|
717
|
+
text: `Here is the documentation for color usage in Primer:\n\n${text}`,
|
|
718
|
+
},
|
|
719
|
+
],
|
|
492
720
|
}
|
|
493
|
-
}
|
|
721
|
+
},
|
|
722
|
+
)
|
|
494
723
|
|
|
495
|
-
|
|
724
|
+
server.registerTool(
|
|
725
|
+
'get_typography_usage',
|
|
726
|
+
{description: 'Get the guidelines for how to apply typography to a user interface'},
|
|
727
|
+
async () => {
|
|
728
|
+
const url = new URL(`/product/getting-started/foundations/typography`, 'https://primer.style')
|
|
729
|
+
const response = await fetch(url)
|
|
730
|
+
if (!response.ok) {
|
|
731
|
+
throw new Error(`Failed to fetch ${url} - ${response.statusText}`)
|
|
732
|
+
}
|
|
496
733
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
{
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
734
|
+
const html = await response.text()
|
|
735
|
+
if (!html) {
|
|
736
|
+
return {
|
|
737
|
+
content: [],
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const $ = cheerio.load(html)
|
|
742
|
+
const source = $('main').html()
|
|
743
|
+
if (!source) {
|
|
744
|
+
return {
|
|
745
|
+
content: [],
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const text = turndownService.turndown(source)
|
|
750
|
+
|
|
751
|
+
return {
|
|
752
|
+
content: [
|
|
753
|
+
{
|
|
754
|
+
type: 'text',
|
|
755
|
+
text: `Here is the documentation for typography usage in Primer:\n\n${text}`,
|
|
756
|
+
},
|
|
757
|
+
],
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
)
|
|
506
761
|
|
|
507
762
|
// -----------------------------------------------------------------------------
|
|
508
763
|
// Icons
|
|
509
764
|
// -----------------------------------------------------------------------------
|
|
510
|
-
server.
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
765
|
+
server.registerTool(
|
|
766
|
+
'list_icons',
|
|
767
|
+
{description: 'List all of the icons (octicons) available from Primer Octicons React'},
|
|
768
|
+
async () => {
|
|
769
|
+
const icons = listIcons().map(icon => {
|
|
770
|
+
const keywords = icon.keywords.map(keyword => {
|
|
771
|
+
return `<keyword>${keyword}</keyword>`
|
|
772
|
+
})
|
|
773
|
+
const sizes = icon.heights.map(height => {
|
|
774
|
+
return `<size value="${height}"></size>`
|
|
775
|
+
})
|
|
776
|
+
return [`<icon name="${icon.name}">`, ...keywords, ...sizes, `</icon>`].join('\n')
|
|
517
777
|
})
|
|
518
|
-
return [`<icon name="${icon.name}">`, ...keywords, ...sizes, `</icon>`].join('\n')
|
|
519
|
-
})
|
|
520
778
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
779
|
+
return {
|
|
780
|
+
content: [
|
|
781
|
+
{
|
|
782
|
+
type: 'text',
|
|
783
|
+
text: `The following icons are available in the @primer/octicons-react package in TypeScript projects:
|
|
526
784
|
|
|
527
785
|
${icons.join('\n')}
|
|
528
786
|
|
|
529
787
|
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.`,
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
788
|
+
},
|
|
789
|
+
],
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
)
|
|
534
793
|
|
|
535
|
-
server.
|
|
794
|
+
server.registerTool(
|
|
536
795
|
'get_icon',
|
|
537
|
-
'Get a specific icon (octicon) by name from Primer',
|
|
538
796
|
{
|
|
539
|
-
|
|
540
|
-
|
|
797
|
+
description: 'Get a specific icon (octicon) by name from Primer',
|
|
798
|
+
inputSchema: {
|
|
799
|
+
name: z.string().describe('The name of the icon to retrieve'),
|
|
800
|
+
size: z.string().optional().describe('The size of the icon to retrieve, e.g. "16"').default('16'),
|
|
801
|
+
},
|
|
541
802
|
},
|
|
542
803
|
async ({name, size}) => {
|
|
543
804
|
const icons = listIcons()
|
|
@@ -593,9 +854,9 @@ ${text}`,
|
|
|
593
854
|
// -----------------------------------------------------------------------------
|
|
594
855
|
// Coding guidelines
|
|
595
856
|
// -----------------------------------------------------------------------------
|
|
596
|
-
server.
|
|
857
|
+
server.registerTool(
|
|
597
858
|
'primer_coding_guidelines',
|
|
598
|
-
'Get the guidelines when writing code that uses Primer or for UI code that you are creating',
|
|
859
|
+
{description: 'Get the guidelines when writing code that uses Primer or for UI code that you are creating'},
|
|
599
860
|
async () => {
|
|
600
861
|
return {
|
|
601
862
|
content: [
|
|
@@ -605,7 +866,7 @@ server.tool(
|
|
|
605
866
|
|
|
606
867
|
## Design Tokens
|
|
607
868
|
|
|
608
|
-
- 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.
|
|
609
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
|
|
610
871
|
|
|
611
872
|
## Authoring & Using Components
|
|
@@ -643,19 +904,15 @@ The following list of coding guidelines must be followed:
|
|
|
643
904
|
*
|
|
644
905
|
*
|
|
645
906
|
**/
|
|
646
|
-
server.
|
|
907
|
+
server.registerTool(
|
|
647
908
|
'review_alt_text',
|
|
648
|
-
'Evaluates image alt text against accessibility best practices and context relevance.',
|
|
649
909
|
{
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
.
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
z.string().describe('The file path of the image src being evaluated'),
|
|
657
|
-
])
|
|
658
|
-
.describe('The image file, file path, or URL being evaluated'),
|
|
910
|
+
description: 'Evaluates image alt text against accessibility best practices and context relevance.',
|
|
911
|
+
inputSchema: {
|
|
912
|
+
surroundingText: z.string().describe('Text surrounding the image, relevant to the image.'),
|
|
913
|
+
alt: z.string().describe('The alt text of the image being evaluated'),
|
|
914
|
+
image: z.string().describe('The image URL or file path being evaluated'),
|
|
915
|
+
},
|
|
659
916
|
},
|
|
660
917
|
async ({surroundingText, alt, image}) => {
|
|
661
918
|
// Call the LLM through MCP sampling
|